From 2e491a7c54ebb77a1552cde4cda7e638af866e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kl=C4=81vs=20Pried=C4=ABtis?= Date: Sun, 16 Feb 2020 19:21:40 +0200 Subject: [PATCH 001/118] feat: drop Link layer in favor of package:gql_link and package:gql_exec BREAKING CHANGE: Link layer is now implemented via package:gql_link and package:gql_exec --- packages/graphql/example/bin/main.dart | 17 +- packages/graphql/example/pubspec.yaml | 4 +- packages/graphql/lib/client.dart | 9 - packages/graphql/lib/internal.dart | 3 - .../legacy_socket_client.dart | 408 ------------- .../legacy_socket_api/legacy_socket_link.dart | 97 ---- .../graphql/lib/src/core/query_manager.dart | 74 +-- .../graphql/lib/src/core/query_options.dart | 9 +- .../lib/src/core/raw_operation_data.dart | 25 +- .../lib/src/exceptions/exceptions.dart | 14 +- .../lib/src/exceptions/graphql_error.dart | 61 -- .../src/exceptions/io_network_exception.dart | 20 - .../exceptions/network_exception_stub.dart | 31 - .../src/exceptions/operation_exception.dart | 2 +- packages/graphql/lib/src/graphql_client.dart | 13 +- .../graphql/lib/src/link/auth/link_auth.dart | 38 -- .../lib/src/link/error/link_error.dart | 70 --- .../graphql/lib/src/link/fetch_result.dart | 19 - .../src/link/http/fallback_http_config.dart | 24 - .../lib/src/link/http/http_config.dart | 43 -- .../graphql/lib/src/link/http/link_http.dart | 373 ------------ .../http/link_http_helper_deprecated_io.dart | 34 -- .../link_http_helper_deprecated_stub.dart | 15 - packages/graphql/lib/src/link/link.dart | 43 -- packages/graphql/lib/src/link/operation.dart | 51 -- .../src/link/web_socket/link_web_socket.dart | 55 -- packages/graphql/lib/src/socket_client.dart | 383 ------------ packages/graphql/lib/src/utilities/file.dart | 3 - .../graphql/lib/src/utilities/file_html.dart | 28 - .../graphql/lib/src/utilities/file_io.dart | 23 - .../graphql/lib/src/utilities/file_stub.dart | 6 - .../graphql/lib/src/websocket/messages.dart | 224 -------- packages/graphql/lib/utilities.dart | 1 - packages/graphql/pubspec.yaml | 8 +- .../test/anonymous_operations_test.dart | 169 +++--- .../graphql/test/graphql_client_test.dart | 207 ++++--- packages/graphql/test/helpers.dart | 14 - .../test/link/error/link_error_test.dart | 110 ---- .../test/link/http/link_http_test.dart | 543 ------------------ packages/graphql/test/link/link_test.dart | 36 -- .../test/multipart_upload_io_test.dart | 94 --- .../graphql/test/multipart_upload_test.dart | 202 ------- packages/graphql/test/socket_client_test.dart | 178 ------ .../test/websocket_legacy_io_test.dart | 96 ---- .../graphql/test/websocket_legacy_test.dart | 54 -- packages/graphql/test/websocket_test.dart | 37 -- .../lib/src/widgets/subscription.dart | 17 +- packages/graphql_flutter/pubspec.yaml | 3 +- 48 files changed, 284 insertions(+), 3704 deletions(-) delete mode 100644 packages/graphql/lib/legacy_socket_api/legacy_socket_client.dart delete mode 100644 packages/graphql/lib/legacy_socket_api/legacy_socket_link.dart delete mode 100644 packages/graphql/lib/src/exceptions/graphql_error.dart delete mode 100644 packages/graphql/lib/src/exceptions/io_network_exception.dart delete mode 100644 packages/graphql/lib/src/exceptions/network_exception_stub.dart delete mode 100644 packages/graphql/lib/src/link/auth/link_auth.dart delete mode 100644 packages/graphql/lib/src/link/error/link_error.dart delete mode 100644 packages/graphql/lib/src/link/fetch_result.dart delete mode 100644 packages/graphql/lib/src/link/http/fallback_http_config.dart delete mode 100644 packages/graphql/lib/src/link/http/http_config.dart delete mode 100644 packages/graphql/lib/src/link/http/link_http.dart delete mode 100644 packages/graphql/lib/src/link/http/link_http_helper_deprecated_io.dart delete mode 100644 packages/graphql/lib/src/link/http/link_http_helper_deprecated_stub.dart delete mode 100644 packages/graphql/lib/src/link/link.dart delete mode 100644 packages/graphql/lib/src/link/operation.dart delete mode 100644 packages/graphql/lib/src/link/web_socket/link_web_socket.dart delete mode 100644 packages/graphql/lib/src/socket_client.dart delete mode 100644 packages/graphql/lib/src/utilities/file.dart delete mode 100644 packages/graphql/lib/src/utilities/file_html.dart delete mode 100644 packages/graphql/lib/src/utilities/file_io.dart delete mode 100644 packages/graphql/lib/src/utilities/file_stub.dart delete mode 100644 packages/graphql/lib/src/websocket/messages.dart delete mode 100644 packages/graphql/lib/utilities.dart delete mode 100644 packages/graphql/test/link/error/link_error_test.dart delete mode 100644 packages/graphql/test/link/http/link_http_test.dart delete mode 100644 packages/graphql/test/link/link_test.dart delete mode 100644 packages/graphql/test/multipart_upload_io_test.dart delete mode 100644 packages/graphql/test/multipart_upload_test.dart delete mode 100644 packages/graphql/test/socket_client_test.dart delete mode 100644 packages/graphql/test/websocket_legacy_io_test.dart delete mode 100644 packages/graphql/test/websocket_legacy_test.dart delete mode 100644 packages/graphql/test/websocket_test.dart diff --git a/packages/graphql/example/bin/main.dart b/packages/graphql/example/bin/main.dart index 2d224c90a..ab184ad68 100644 --- a/packages/graphql/example/bin/main.dart +++ b/packages/graphql/example/bin/main.dart @@ -1,10 +1,13 @@ import 'dart:io' show stdout, stderr, exit; import 'package:args/args.dart'; +import 'package:gql_http_link/gql_http_link.dart'; +import 'package:gql_link/gql_link.dart'; import 'package:graphql/client.dart'; import './graphql_operation/mutations/mutations.dart'; import './graphql_operation/queries/readRepositories.dart'; + // to run the example, create a file ../local.dart with the content: // const String YOUR_PERSONAL_ACCESS_TOKEN = // ''; @@ -15,17 +18,13 @@ ArgResults argResults; // client - create a graphql client GraphQLClient client() { - final HttpLink _httpLink = HttpLink( - uri: 'https://api.github.com/graphql', - ); - - final AuthLink _authLink = AuthLink( - // ignore: undefined_identifier - getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', + final Link _link = HttpLink( + 'https://api.github.com/graphql', + defaultHeaders: { + 'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', + }, ); - final Link _link = _authLink.concat(_httpLink); - return GraphQLClient( cache: InMemoryCache(), link: _link, diff --git a/packages/graphql/example/pubspec.yaml b/packages/graphql/example/pubspec.yaml index a73bdaee6..fce16de6b 100644 --- a/packages/graphql/example/pubspec.yaml +++ b/packages/graphql/example/pubspec.yaml @@ -6,7 +6,9 @@ environment: sdk: ">=2.6.0 <3.0.0" dependencies: - args: + args: + gql_link: ^0.2.3 + gql_http_link: ^0.2.7 graphql: path: .. diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index a11f2b297..00c91ddaa 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -10,12 +10,3 @@ export 'package:graphql/src/core/query_options.dart'; export 'package:graphql/src/core/query_result.dart'; export 'package:graphql/src/exceptions/exceptions.dart'; export 'package:graphql/src/graphql_client.dart'; -export 'package:graphql/src/link/auth/link_auth.dart'; -export 'package:graphql/src/link/error/link_error.dart'; -export 'package:graphql/src/link/fetch_result.dart'; -export 'package:graphql/src/link/http/link_http.dart'; -export 'package:graphql/src/link/link.dart'; -export 'package:graphql/src/link/operation.dart'; -export 'package:graphql/src/link/web_socket/link_web_socket.dart'; -export 'package:graphql/src/socket_client.dart'; -export 'package:graphql/src/websocket/messages.dart'; diff --git a/packages/graphql/lib/internal.dart b/packages/graphql/lib/internal.dart index 79b90c4c1..d38c2a107 100644 --- a/packages/graphql/lib/internal.dart +++ b/packages/graphql/lib/internal.dart @@ -1,6 +1,3 @@ export 'package:graphql/src/utilities/helpers.dart'; export 'package:graphql/src/core/observable_query.dart'; - -export 'package:graphql/src/link/operation.dart'; -export 'package:graphql/src/link/fetch_result.dart'; diff --git a/packages/graphql/lib/legacy_socket_api/legacy_socket_client.dart b/packages/graphql/lib/legacy_socket_api/legacy_socket_client.dart deleted file mode 100644 index b47f5ccb2..000000000 --- a/packages/graphql/lib/legacy_socket_api/legacy_socket_client.dart +++ /dev/null @@ -1,408 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:meta/meta.dart'; -import 'package:rxdart/rxdart.dart'; - -import 'package:uuid_enhanced/uuid.dart'; - -import 'package:graphql/src/websocket/messages.dart'; - -enum SocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED } - -@deprecated -class SocketClientConfig { - const SocketClientConfig({ - this.autoReconnect = true, - this.queryAndMutationTimeout = const Duration(seconds: 10), - this.inactivityTimeout = const Duration(seconds: 30), - this.delayBetweenReconnectionAttempts = const Duration(seconds: 5), - this.compression = CompressionOptions.compressionDefault, - this.initPayload, - // @todo please review this ignore rule - // ignore: deprecated_member_use_from_same_package - @deprecated this.legacyInitPayload, - }); - - /// Whether to reconnect to the server after detecting connection loss. - final bool autoReconnect; - - /// The duration after which the connection is considered unstable, because no keep alive message - /// was received from the server in the given time-frame. The connection to the server will be closed. - /// If [autoReconnect] is set to true, we try to reconnect to the server after the specified [delayBetweenReconnectionAttempts]. - /// - /// If null, the keep alive messages will be ignored. - final Duration inactivityTimeout; - - /// The duration that needs to pass before trying to reconnect to the server after a connection loss. - /// This only takes effect when [autoReconnect] is set to true. - /// - /// If null, the reconnection will occur immediately, although not recommended. - final Duration delayBetweenReconnectionAttempts; - - // The duration after which a query or mutation should time out. - // If null, no timeout is applied, although not recommended. - final Duration queryAndMutationTimeout; - - final CompressionOptions compression; - - /// The initial payload that will be sent to the server upon connection. - /// Can be null, but must be a valid json structure if provided. - final dynamic initPayload; - - final Map legacyInitPayload; - - InitOperation get initOperation { - if (legacyInitPayload != null) { - print( - 'WARNING: Using a legacyInitPayload which will be removed soon. ' - 'If you need this particular payload serialization behavior, ' - 'please comment on this issue with details on your usecase: ' - 'https://github.com/zino-app/graphql-flutter/pull/277', - ); - // @todo please review this ignore rule - // ignore: deprecated_member_use_from_same_package - return LegacyInitOperation(legacyInitPayload); - } - return InitOperation(initPayload); - } -} - -/// @deprecated Old SocketClient that accepts and handles headers. -/// WebSocket headers are not usable in the browser. -/// So, to encourage universality, -/// they are not usable in the main socket link and client any longer -/// -/// Wraps a standard web socket instance to marshal and un-marshal the server / -/// client payloads into dart object representation. -/// -/// This class also deals with reconnection, handles timeout and keep alive messages. -/// -/// It is meant to be instantiated once, and you can let this class handle all the heavy- -/// lifting of socket state management. Once you're done with the socket connection, make sure -/// you call the [dispose] method to release all allocated resources. -@deprecated -class SocketClient { - SocketClient( - this.url, { - this.protocols = const [ - 'graphql-ws', - ], - this.headers = const { - 'content-type': 'application/json', - }, - this.config = const SocketClientConfig(), - @visibleForTesting this.randomBytesForUuid, - }) { - _connect(); - } - - Uint8List randomBytesForUuid; - final String url; - final SocketClientConfig config; - final Iterable protocols; - final Map headers; - final BehaviorSubject _connectionStateController = - BehaviorSubject(); - - Timer _reconnectTimer; - WebSocket _socket; - - @visibleForTesting - WebSocket get socket => _socket; - - Stream _stream; - - @visibleForTesting - @protected - Stream get stream => _stream ??= _socket.asBroadcastStream(); - - Stream _messageStream; - - StreamSubscription _keepAliveSubscription; - StreamSubscription _messageSubscription; - - /// Connects to the server. - /// - /// If this instance is disposed, this method does nothing. - Future _connect() async { - if (_connectionStateController.isClosed) { - return; - } - - _connectionStateController.value = SocketConnectionState.CONNECTING; - print('Connecting to websocket: $url...'); - - try { - _socket = await WebSocket.connect(url, - protocols: protocols, - headers: headers, - compression: config.compression); - _connectionStateController.value = SocketConnectionState.CONNECTED; - print('Connected to websocket.'); - _write(config.initOperation); - - _stream ??= _socket.asBroadcastStream(); - _messageStream = _stream.map(_parseSocketMessage); - - if (config.inactivityTimeout != null) { - _keepAliveSubscription = _messagesOfType().timeout( - config.inactivityTimeout, - onTimeout: (EventSink event) { - print( - "Haven't received keep alive message for ${config.inactivityTimeout.inSeconds} seconds. Disconnecting.."); - event.close(); - _socket.close(WebSocketStatus.goingAway); - _connectionStateController.value = - SocketConnectionState.NOT_CONNECTED; - }, - ).listen(null); - } - - _messageSubscription = _messageStream.listen( - (dynamic data) { - // print('data: $data'); - }, - onDone: () { - // print('done'); - onConnectionLost(); - }, - cancelOnError: true, - onError: (dynamic e) { - print('error: $e'); - }); - } catch (e) { - onConnectionLost(); - } - } - - void onConnectionLost() { - print('Disconnected from websocket.'); - _reconnectTimer?.cancel(); - _keepAliveSubscription?.cancel(); - _messageSubscription?.cancel(); - - if (_connectionStateController.isClosed) { - return; - } - - if (_connectionStateController.value != - SocketConnectionState.NOT_CONNECTED) { - _connectionStateController.value = SocketConnectionState.NOT_CONNECTED; - } - - if (config.autoReconnect && !_connectionStateController.isClosed) { - if (config.delayBetweenReconnectionAttempts != null) { - print( - 'Scheduling to connect in ${config.delayBetweenReconnectionAttempts.inSeconds} seconds...'); - - _reconnectTimer = Timer( - config.delayBetweenReconnectionAttempts, - () { - _connect(); - }, - ); - } else { - Timer.run(() => _connect()); - } - } - } - - /// Closes the underlying socket if connected, and stops reconnection attempts. - /// After calling this method, this [SocketClient] instance must be considered - /// unusable. Instead, create a new instance of this class. - /// - /// Use this method if you'd like to disconnect from the specified server permanently, - /// and you'd like to connect to another server instead of the current one. - Future dispose() async { - print('Disposing socket client..'); - _reconnectTimer?.cancel(); - await _socket?.close(); - await _keepAliveSubscription?.cancel(); - await _messageSubscription?.cancel(); - await _connectionStateController?.close(); - _stream = null; - } - - static GraphQLSocketMessage _parseSocketMessage(dynamic message) { - final Map map = - json.decode(message as String) as Map; - final String type = (map['type'] ?? 'unknown') as String; - final dynamic payload = map['payload'] ?? {}; - final String id = (map['id'] ?? 'none') as String; - - switch (type) { - case MessageTypes.GQL_CONNECTION_ACK: - return ConnectionAck(); - case MessageTypes.GQL_CONNECTION_ERROR: - return ConnectionError(payload); - case MessageTypes.GQL_CONNECTION_KEEP_ALIVE: - return ConnectionKeepAlive(); - case MessageTypes.GQL_DATA: - final dynamic data = payload['data']; - final dynamic errors = payload['errors']; - return SubscriptionData(id, data, errors); - case MessageTypes.GQL_ERROR: - return SubscriptionError(id, payload); - case MessageTypes.GQL_COMPLETE: - return SubscriptionComplete(id); - default: - return UnknownData(map); - } - } - - void _write(final GraphQLSocketMessage message) { - if (_connectionStateController.value == SocketConnectionState.CONNECTED) { - _socket.add( - json.encode( - message, - toEncodable: (dynamic m) => m.toJson(), - ), - ); - } - } - - /// Sends a query, mutation or subscription request to the server, and returns a stream of the response. - /// - /// If the request is a query or mutation, a timeout will be applied to the request as specified by - /// [SocketClientConfig]'s [queryAndMutationTimeout] field. - /// - /// If the request is a subscription, obviously no timeout is applied. - /// - /// In case of socket disconnection, the returned stream will be closed. - Stream subscribe( - final SubscriptionRequest payload, final bool waitForConnection) { - final String id = Uuid.randomUuid(random: randomBytesForUuid).toString(); - final StreamController response = - StreamController(); - StreamSubscription sub; - final bool addTimeout = !payload.operation.isSubscription && - config.queryAndMutationTimeout != null; - - response.onListen = () { - final Stream waitForConnectedStateWithoutTimeout = - _connectionStateController - .startWith( - waitForConnection ? null : SocketConnectionState.CONNECTED) - .where((SocketConnectionState state) => - state == SocketConnectionState.CONNECTED) - .take(1); - - final Stream waitForConnectedState = addTimeout - ? waitForConnectedStateWithoutTimeout.timeout( - config.queryAndMutationTimeout, - onTimeout: (EventSink event) { - print('Connection timed out.'); - response.addError(TimeoutException('Connection timed out.')); - event.close(); - response.close(); - }, - ) - : waitForConnectedStateWithoutTimeout; - - sub = waitForConnectedState.listen((_) { - final Stream dataErrorComplete = - _messageStream.where( - (GraphQLSocketMessage message) { - if (message is SubscriptionData) { - return message.id == id; - } - - if (message is SubscriptionError) { - return message.id == id; - } - - if (message is SubscriptionComplete) { - return message.id == id; - } - - return false; - }, - ).takeWhile((_) => !response.isClosed); - - final Stream subscriptionComplete = addTimeout - ? dataErrorComplete - .where((GraphQLSocketMessage message) => - message is SubscriptionComplete) - .take(1) - .timeout( - config.queryAndMutationTimeout, - onTimeout: (EventSink event) { - print('Request timed out.'); - response.addError(TimeoutException('Request timed out.')); - event.close(); - response.close(); - }, - ) - : dataErrorComplete - .where((GraphQLSocketMessage message) => - message is SubscriptionComplete) - .take(1); - - subscriptionComplete.listen((_) => response.close()); - - dataErrorComplete - .where( - (GraphQLSocketMessage message) => message is SubscriptionData) - .cast() - .listen((SubscriptionData message) => response.add(message)); - - dataErrorComplete - .where( - (GraphQLSocketMessage message) => message is SubscriptionError) - .listen( - (GraphQLSocketMessage message) => response.addError(message)); - - _write(StartOperation(id, payload)); - }); - }; - - response.onCancel = () { - sub?.cancel(); - if (_connectionStateController.value == SocketConnectionState.CONNECTED && - _socket != null) { - _write(StopOperation(id)); - } - }; - - return response.stream; - } - - /// These streams will emit done events when the current socket is done. - - /// A stream that emits the last value of the connection state upon subscription. - Stream get connectionState => - _connectionStateController.stream; - - /// Filter `_messageStream` for messages of the given type of [GraphQLSocketMessage] - /// - /// Example usages: - /// `_messagesOfType()` for init acknowledgments - /// `_messagesOfType()` for errors - /// `_messagesOfType()` for unknown data messages - Stream _messagesOfType() => _messageStream - .where((GraphQLSocketMessage message) => message is M) - .cast(); -} - -/// The old implementation of [InitOperation] -// @todo please review this ignore rule -// ignore: deprecated_member_use_from_same_package -@deprecated -class LegacyInitOperation extends InitOperation { - LegacyInitOperation(dynamic payload) : super(payload); - - @override - Map toJson() { - final Map jsonMap = {}; - jsonMap['type'] = type; - - if (payload != null) { - jsonMap['payload'] = json.encode(payload); - } - - return jsonMap; - } -} diff --git a/packages/graphql/lib/legacy_socket_api/legacy_socket_link.dart b/packages/graphql/lib/legacy_socket_api/legacy_socket_link.dart deleted file mode 100644 index e72109fd7..000000000 --- a/packages/graphql/lib/legacy_socket_api/legacy_socket_link.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:meta/meta.dart'; -import 'package:graphql/src/link/fetch_result.dart'; -import 'package:graphql/src/link/link.dart'; -import 'package:graphql/src/link/operation.dart'; -import 'package:graphql/src/socket_client.dart'; -import 'package:graphql/src/websocket/messages.dart'; - -/// @deprecated Old SocketClient that accepts and handles headers. -/// WebSocket headers are not usable in the browser. -/// So, to encourage universality, -/// they are not usable in the main socket link and client any longer -/// -/// A websocket [Link] implementation to support the websocket transport. -/// It supports subscriptions, query and mutation operations as well. -/// -/// This link is aware of [AuthLink], so the headers specified there are automatically -/// applied when connecting to the socket server, unless explicitly overridden by the [headers] -/// parameter. -/// -/// There's an option called [reconnectOnHeaderChange] that makes it possible to reconnect to the server when the -/// headers have changed. For example, if the user logs in with another user account and the `Authorization` header changes. -/// This could be desired because of the nature of websocket connections: headers can only be specified upon connecting. -/// -/// NOTE: the actual socket connection will only get established after an [Operation] is handled by this [WebSocketLink]. -/// If you'd like to connect to the socket server instantly, call the [connectOrReconnect] method after creating this [WebSocketLink] instance. -@deprecated -class WebSocketLink extends Link { - /// Creates a new [WebSocketLink] instance with the specified config. - WebSocketLink({ - @required this.url, - this.headers, - this.reconnectOnHeaderChange = true, - this.config = const SocketClientConfig(), - }) : super() { - if (headers != null) { - print( - 'WARNING: Using direct websocket headers which will be removed soon, ' - 'as it is incompatable with dart:html. ' - 'If you need this direct header access, ' - 'please comment on this PR with details on your usecase: ' - 'https://github.com/zino-app/graphql-flutter/pull/323', - ); - } else { - print( - 'WARNING: You are using the deprecated websocket API, ' - 'but do not appear to need direct header access. ' - 'If you also do not need the legacyInitPayload, ' - 'please switch to the new link and client', - ); - } - request = _doOperation; - } - - final String url; - final Map headers; - final bool reconnectOnHeaderChange; - final SocketClientConfig config; - - // cannot be final because we're changing the instance upon a header change. - SocketClient _socketClient; - - Stream _doOperation(Operation operation, [NextLink forward]) { - final Map concatHeaders = {}; - final Map context = operation.getContext(); - if (context != null && context.containsKey('headers')) { - concatHeaders.addAll(context['headers'] as Map); - } - // @todo deprecated - if (headers != null) { - concatHeaders.addAll(headers); - } - - if (_socketClient == null) { - connectOrReconnect(headers: concatHeaders); - } - - return _socketClient.subscribe(SubscriptionRequest(operation), true).map( - (SubscriptionData result) => FetchResult( - data: result.data, - errors: result.errors as List, - context: operation.getContext(), - extensions: operation.extensions)); - } - - /// Connects or reconnects to the server with the specified headers. - void connectOrReconnect({Map headers}) { - _socketClient?.dispose(); - _socketClient = SocketClient(url, config: config); - } - - /// Disposes the underlying socket client explicitly. Only use this, if you want to disconnect from - /// the current server in favour of another one. If that's the case, create a new [WebSocketLink] instance. - Future dispose() async { - await _socketClient?.dispose(); - _socketClient = null; - } -} diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 73c224b17..fdad03aa7 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:gql_exec/gql_exec.dart'; +import 'package:gql_link/gql_link.dart'; import 'package:graphql/src/cache/cache.dart'; import 'package:graphql/src/cache/normalized_in_memory.dart' show NormalizedInMemoryCache; @@ -8,9 +10,6 @@ import 'package:graphql/src/core/observable_query.dart'; import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/query_result.dart'; import 'package:graphql/src/exceptions/exceptions.dart'; -import 'package:graphql/src/link/fetch_result.dart'; -import 'package:graphql/src/link/link.dart'; -import 'package:graphql/src/link/operation.dart'; import 'package:graphql/src/scheduler/scheduler.dart'; import 'package:meta/meta.dart'; @@ -104,35 +103,44 @@ class QueryManager { String queryId, BaseOptions options, ) async { - // create a new operation to fetch - final Operation operation = Operation.fromOptions(options) - ..setContext(options.context); + // create a new request to execute + final Request request = Request( + operation: Operation( + document: options.documentNode, + operationName: options.operationName, + ), + variables: options.variables, + context: options.context ?? Context(), + ); - FetchResult fetchResult; + Response response; QueryResult queryResult; try { - // execute the operation through the provided link(s) - fetchResult = await execute( - link: link, - operation: operation, - ).first; - - // save the data from fetchResult to the cache - if (fetchResult.data != null && - options.fetchPolicy != FetchPolicy.noCache) { + // execute the request through the provided link(s) + response = await link + .request( + request, + ) + .first; + + // save the data from response to the cache + if (response.data != null && options.fetchPolicy != FetchPolicy.noCache) { cache.write( - operation.toKey(), - fetchResult.data, + // TODO: think of an alternative to the old toKey(), + request.hashCode.toString(), + response.data, ); } queryResult = mapFetchResultToQueryResult( - fetchResult, + response, options, source: QueryResultSource.Network, ); } catch (failure) { + // TODO: handle Link exceptions + // we set the source to indicate where the source of failure queryResult ??= QueryResult(source: QueryResultSource.Network); @@ -147,7 +155,10 @@ class QueryManager { if (options.fetchPolicy != FetchPolicy.noCache && cache is NormalizedInMemoryCache) { // normalize results if previously written - queryResult.data = cache.read(operation.toKey()); + queryResult.data = cache.read( + // TODO: think of an alternative to the old toKey(), + request.hashCode.toString(), + ); } addQueryResult(queryId, queryResult); @@ -193,7 +204,7 @@ class QueryManager { source: QueryResultSource.Cache, exception: OperationException( clientException: CacheMissException( - 'Could not find that operation in the cache. (FetchPolicy.cacheOnly)', + 'Could not find that request in the cache. (FetchPolicy.cacheOnly)', cacheKey, ), ), @@ -287,7 +298,7 @@ class QueryManager { if (cachedData != null) { query.addResult( mapFetchResultToQueryResult( - FetchResult(data: cachedData), + Response(data: cachedData), query.options, source: QueryResultSource.Cache, ), @@ -317,7 +328,7 @@ class QueryManager { } QueryResult mapFetchResultToQueryResult( - FetchResult fetchResult, + Response response, BaseOptions options, { @required QueryResultSource source, }) { @@ -326,26 +337,26 @@ class QueryManager { // check if there are errors and apply the error policy if so // in a nutshell: `ignore` swallows errors, `none` swallows data - if (fetchResult.errors != null && fetchResult.errors.isNotEmpty) { + if (response.errors != null && response.errors.isNotEmpty) { switch (options.errorPolicy) { case ErrorPolicy.all: // handle both errors and data - errors = _errorsFromResult(fetchResult); - data = fetchResult.data; + errors = response.errors; + data = response.data; break; case ErrorPolicy.ignore: // ignore errors - data = fetchResult.data; + data = response.data; break; case ErrorPolicy.none: default: // TODO not actually sure if apollo even casts graphql errors in `none` mode, // it's also kind of legacy - errors = _errorsFromResult(fetchResult); + errors = response.errors; break; } } else { - data = fetchResult.data; + data = response.data; } return QueryResult( @@ -354,9 +365,4 @@ class QueryManager { exception: coalesceErrors(graphqlErrors: errors), ); } - - List _errorsFromResult(FetchResult fetchResult) => - List.from(fetchResult.errors.map( - (dynamic rawError) => GraphQLError.fromJSON(rawError), - )); } diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index fd99be102..6605b92a9 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -1,5 +1,6 @@ import 'package:gql/ast.dart'; import 'package:gql/language.dart'; +import 'package:gql_exec/gql_exec.dart'; import 'package:graphql/client.dart'; import 'package:graphql/internal.dart'; import 'package:graphql/src/core/raw_operation_data.dart'; @@ -101,7 +102,7 @@ class BaseOptions extends RawOperationData { ErrorPolicy get errorPolicy => policies.error; /// Context to be passed to link execution chain. - Map context; + Context context; } /// Query options. @@ -115,7 +116,7 @@ class QueryOptions extends BaseOptions { ErrorPolicy errorPolicy, Object optimisticResult, this.pollInterval, - Map context, + Context context, }) : super( policies: Policies(fetch: fetchPolicy, error: errorPolicy), // ignore: deprecated_member_use_from_same_package @@ -144,7 +145,7 @@ class MutationOptions extends BaseOptions { Map variables, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, - Map context, + Context context, this.onCompleted, this.update, this.onError, @@ -262,7 +263,7 @@ class WatchQueryOptions extends QueryOptions { int pollInterval, this.fetchResults = false, this.eagerlyFetchResults, - Map context, + Context context, }) : super( // ignore: deprecated_member_use_from_same_package document: document, diff --git a/packages/graphql/lib/src/core/raw_operation_data.dart b/packages/graphql/lib/src/core/raw_operation_data.dart index a3c4a16e2..5d003a9e8 100644 --- a/packages/graphql/lib/src/core/raw_operation_data.dart +++ b/packages/graphql/lib/src/core/raw_operation_data.dart @@ -3,10 +3,7 @@ import 'dart:convert' show json; import 'package:gql/ast.dart'; import 'package:gql/language.dart'; -import 'package:graphql/src/link/http/link_http_helper_deprecated_stub.dart' - if (dart.library.io) 'package:graphql/src/link/http/link_http_helper_deprecated_io.dart'; import 'package:graphql/src/utilities/get_from_ast.dart'; -import 'package:http/http.dart'; class RawOperationData { RawOperationData({ @@ -74,20 +71,14 @@ class RawOperationData { String toKey() { /// SplayTreeMap is always sorted - final String encodedVariables = - json.encode(variables, toEncodable: (dynamic object) { - if (object is MultipartFile) { - return object.filename; - } - // @deprecated, backward compatible only - // in case the body is io.File - // in future release, io.File will no longer be supported - if (isIoFile(object)) { - return object.path; - } - // default toEncodable behavior - return object.toJson(); - }); + final String encodedVariables = json.encode( + variables, + toEncodable: (dynamic object) { + // TODO: transparently handle multipart file without introducing package:http + // default toEncodable behavior + return object.toJson(); + }, + ); // TODO: document is being depracated, find ways for generating key // ignore: deprecated_member_use_from_same_package diff --git a/packages/graphql/lib/src/exceptions/exceptions.dart b/packages/graphql/lib/src/exceptions/exceptions.dart index 80bc1bb5e..8875df2b8 100644 --- a/packages/graphql/lib/src/exceptions/exceptions.dart +++ b/packages/graphql/lib/src/exceptions/exceptions.dart @@ -1,14 +1,2 @@ -import 'package:graphql/src/exceptions/_base_exceptions.dart' as _b; -import 'package:graphql/src/exceptions/io_network_exception.dart' as _n; - -export 'package:graphql/src/exceptions/_base_exceptions.dart' - hide translateFailure; -export 'package:graphql/src/exceptions/graphql_error.dart'; +export 'package:graphql/src/exceptions/_base_exceptions.dart'; export 'package:graphql/src/exceptions/operation_exception.dart'; -export 'package:graphql/src/exceptions/network_exception_stub.dart' - if (dart.library.io) 'package:graphql/src/exceptions/io_network_exception.dart' - hide translateNetworkFailure; - -_b.ClientException translateFailure(dynamic failure) { - return _n.translateNetworkFailure(failure) ?? _b.translateFailure(failure); -} diff --git a/packages/graphql/lib/src/exceptions/graphql_error.dart b/packages/graphql/lib/src/exceptions/graphql_error.dart deleted file mode 100644 index a3940b102..000000000 --- a/packages/graphql/lib/src/exceptions/graphql_error.dart +++ /dev/null @@ -1,61 +0,0 @@ -/// A location where a [GraphQLError] appears. -class Location { - /// Constructs a [Location] from a JSON map. - Location.fromJSON(Map data) - : line = data['line'], - column = data['column']; - - /// The line of the error in the query. - final int line; - - /// The column of the error in the query. - final int column; - - @override - String toString() => '{ line: $line, column: $column }'; -} - -/// A GraphQL error (returned by a GraphQL server). -class GraphQLError { - GraphQLError({ - this.raw, - this.message, - this.locations, - this.path, - this.extensions, - }); - - /// Constructs a [GraphQLError] from a JSON map. - GraphQLError.fromJSON(this.raw) - : message = raw['message'] is String - ? raw['message'] as String - : 'Invalid server response: message property needs to be of type String', - locations = raw['locations'] is List> - ? List.from( - (raw['locations'] as List>).map( - (Map location) => Location.fromJSON(location), - ), - ) - : null, - path = raw['path'] as List, - extensions = raw['extensions'] as Map; - - /// The message of the error. - final dynamic raw; - - /// The message of the error. - final String message; - - /// Locations where the error appear. - final List locations; - - /// The path of the field in error. - final List path; - - /// Custom error data returned by your GraphQL API server - final Map extensions; - - @override - String toString() => - '$message: ${locations is List ? locations.map((Location l) => '[${l.toString()}]').join('') : "Undefined location"}'; -} diff --git a/packages/graphql/lib/src/exceptions/io_network_exception.dart b/packages/graphql/lib/src/exceptions/io_network_exception.dart deleted file mode 100644 index 2ac59fb83..000000000 --- a/packages/graphql/lib/src/exceptions/io_network_exception.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:io' show SocketException; -import 'dart:io'; -import './network_exception_stub.dart' as stub; - -export './network_exception_stub.dart' show NetworkException; - -stub.NetworkException translateNetworkFailure(dynamic failure) { - if (failure is SocketException) { - return stub.NetworkException( - wrappedException: failure, - message: failure.message, - uri: Uri( - scheme: 'http', - host: failure.address?.host, - port: failure.port, - ), - ); - } - return stub.translateNetworkFailure(failure); -} diff --git a/packages/graphql/lib/src/exceptions/network_exception_stub.dart b/packages/graphql/lib/src/exceptions/network_exception_stub.dart deleted file mode 100644 index 52a6a6f21..000000000 --- a/packages/graphql/lib/src/exceptions/network_exception_stub.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:http/http.dart' as http; - -import './_base_exceptions.dart' show ClientException; - -class NetworkException implements ClientException { - covariant Exception wrappedException; - - String message; - - Uri uri; - - NetworkException({ - this.wrappedException, - this.message, - this.uri, - }); - - String toString() => - 'Failed to connect to $uri: ${message ?? wrappedException}'; -} - -NetworkException translateNetworkFailure(dynamic failure) { - if (failure is http.ClientException) { - return NetworkException( - wrappedException: failure, - message: failure.message, - uri: failure.uri, - ); - } - return null; -} diff --git a/packages/graphql/lib/src/exceptions/operation_exception.dart b/packages/graphql/lib/src/exceptions/operation_exception.dart index 2be0b531c..ca9a84e70 100644 --- a/packages/graphql/lib/src/exceptions/operation_exception.dart +++ b/packages/graphql/lib/src/exceptions/operation_exception.dart @@ -1,5 +1,5 @@ +import 'package:gql_exec/gql_exec.dart'; import 'package:graphql/src/exceptions/_base_exceptions.dart'; -import './graphql_error.dart'; class OperationException implements Exception { /// Any graphql errors returned from the operation diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 48725bf19..a2c8ac022 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -1,13 +1,12 @@ import 'dart:async'; +import 'package:gql_exec/gql_exec.dart'; +import 'package:gql_link/gql_link.dart'; import 'package:graphql/src/cache/cache.dart'; import 'package:graphql/src/core/observable_query.dart'; import 'package:graphql/src/core/query_manager.dart'; import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/query_result.dart'; -import 'package:graphql/src/link/fetch_result.dart'; -import 'package:graphql/src/link/link.dart'; -import 'package:graphql/src/link/operation.dart'; import 'package:meta/meta.dart'; /// The default [Policies] to set for each client action @@ -64,7 +63,7 @@ class DefaultPolicies { ); } -/// The link is a [Link] over which GraphQL documents will be resolved into a [FetchResult]. +/// The link is a [Link] over which GraphQL documents will be resolved into a [Response]. /// The cache is the initial [Cache] to use in the data store. class GraphQLClient { /// Constructs a [GraphQLClient] given a [Link] and a [Cache]. @@ -83,7 +82,7 @@ class GraphQLClient { /// The default [Policies] to set for each client action DefaultPolicies defaultPolicies; - /// The [Link] over which GraphQL documents will be resolved into a [FetchResult]. + /// The [Link] over which GraphQL documents will be resolved into a [Response]. final Link link; /// The initial [Cache] to use in the data store. @@ -115,7 +114,7 @@ class GraphQLClient { /// This subscribes to a GraphQL subscription according to the options specified and returns a /// [Stream] which either emits received data or an error. - Stream subscribe(Operation operation) { - return execute(link: link, operation: operation); + Stream subscribe(Request request) { + return link.request(request); } } diff --git a/packages/graphql/lib/src/link/auth/link_auth.dart b/packages/graphql/lib/src/link/auth/link_auth.dart deleted file mode 100644 index ab2ba010e..000000000 --- a/packages/graphql/lib/src/link/auth/link_auth.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'dart:async'; - -import 'package:graphql/src/link/link.dart'; -import 'package:graphql/src/link/operation.dart'; -import 'package:graphql/src/link/fetch_result.dart'; - -typedef GetToken = FutureOr Function(); - -class AuthLink extends Link { - AuthLink({ - this.getToken, - }) : super( - request: (Operation operation, [NextLink forward]) { - StreamController controller; - - Future onListen() async { - try { - final String token = await getToken(); - - operation.setContext(>{ - 'headers': {'Authorization': token} - }); - } catch (error) { - controller.addError(error); - } - - await controller.addStream(forward(operation)); - await controller.close(); - } - - controller = StreamController(onListen: onListen); - - return controller.stream; - }, - ); - - GetToken getToken; -} diff --git a/packages/graphql/lib/src/link/error/link_error.dart b/packages/graphql/lib/src/link/error/link_error.dart deleted file mode 100644 index d28e621b7..000000000 --- a/packages/graphql/lib/src/link/error/link_error.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:async'; - -import 'package:graphql/src/link/link.dart'; -import 'package:graphql/src/link/operation.dart'; -import 'package:graphql/src/link/fetch_result.dart'; -import 'package:graphql/src/exceptions/exceptions.dart'; -import 'package:graphql/src/exceptions/graphql_error.dart'; -import 'package:graphql/src/exceptions/operation_exception.dart'; - -typedef ErrorHandler = void Function(ErrorResponse); - -class ErrorResponse { - ErrorResponse({ - this.operation, - this.fetchResult, - this.exception, - }); - - Operation operation; - FetchResult fetchResult; - OperationException exception; -} - -class ErrorLink extends Link { - ErrorLink({ - this.errorHandler, - }) : super( - request: (Operation operation, [NextLink forward]) { - StreamController controller; - - Future onListen() async { - Stream stream = forward(operation).map((FetchResult fetchResult) { - if (fetchResult.errors != null) { - List errors = fetchResult.errors - .map((json) => GraphQLError.fromJSON(json)) - .toList(); - - ErrorResponse response = ErrorResponse( - operation: operation, - fetchResult: fetchResult, - exception: OperationException(graphqlErrors: errors), - ); - - errorHandler(response); - } - return fetchResult; - }).handleError((error) { - ErrorResponse response = ErrorResponse( - operation: operation, - exception: OperationException( - clientException: translateFailure(error), - ), - ); - - errorHandler(response); - throw error; - }); - - await controller.addStream(stream); - await controller.close(); - } - - controller = StreamController(onListen: onListen); - - return controller.stream; - }, - ); - - ErrorHandler errorHandler; -} diff --git a/packages/graphql/lib/src/link/fetch_result.dart b/packages/graphql/lib/src/link/fetch_result.dart deleted file mode 100644 index 0a6f7383e..000000000 --- a/packages/graphql/lib/src/link/fetch_result.dart +++ /dev/null @@ -1,19 +0,0 @@ -class FetchResult { - FetchResult({ - this.statusCode, - this.reasonPhrase, - this.errors, - this.data, - this.extensions, - this.context, - }); - int statusCode; - String reasonPhrase; - - List errors; - - /// List or Map - dynamic data; - Map extensions; - Map context; -} diff --git a/packages/graphql/lib/src/link/http/fallback_http_config.dart b/packages/graphql/lib/src/link/http/fallback_http_config.dart deleted file mode 100644 index 0dcabea6f..000000000 --- a/packages/graphql/lib/src/link/http/fallback_http_config.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:graphql/src/link/http/http_config.dart'; - -HttpQueryOptions defaultHttpOptions = HttpQueryOptions( - includeQuery: true, - includeExtensions: false, -); - -Map defaultOptions = { - 'method': 'POST', -}; - -Map defaultHeaders = { - 'accept': '*/*', - 'content-type': 'application/json', -}; - -Map defaultCredentials = {}; - -HttpConfig fallbackHttpConfig = HttpConfig( - http: defaultHttpOptions, - options: defaultOptions, - headers: defaultHeaders, - credentials: defaultCredentials, -); diff --git a/packages/graphql/lib/src/link/http/http_config.dart b/packages/graphql/lib/src/link/http/http_config.dart deleted file mode 100644 index ea3682f14..000000000 --- a/packages/graphql/lib/src/link/http/http_config.dart +++ /dev/null @@ -1,43 +0,0 @@ -class HttpQueryOptions { - HttpQueryOptions({ - this.includeQuery, - this.includeExtensions, - }); - - bool includeQuery; - bool includeExtensions; - - void addAll(HttpQueryOptions options) { - if (options.includeQuery != null) { - includeQuery = options.includeQuery; - } - - if (options.includeExtensions != null) { - includeExtensions = options.includeExtensions; - } - } -} - -class HttpConfig { - HttpConfig({ - this.http, - this.options, - this.credentials, - this.headers, - }); - - HttpQueryOptions http; - Map options; - Map credentials; - Map headers; -} - -class HttpHeadersAndBody { - HttpHeadersAndBody({ - this.headers, - this.body, - }); - - final Map headers; - final Map body; -} diff --git a/packages/graphql/lib/src/link/http/link_http.dart b/packages/graphql/lib/src/link/http/link_http.dart deleted file mode 100644 index 394ab79b6..000000000 --- a/packages/graphql/lib/src/link/http/link_http.dart +++ /dev/null @@ -1,373 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:graphql/src/exceptions/exceptions.dart' as ex; -import 'package:meta/meta.dart'; -import 'package:http/http.dart'; -import 'package:http_parser/http_parser.dart'; - -import 'package:gql/language.dart'; -import 'package:graphql/src/utilities/helpers.dart' show notNull; -import 'package:graphql/src/link/link.dart'; -import 'package:graphql/src/link/operation.dart'; -import 'package:graphql/src/link/fetch_result.dart'; -import 'package:graphql/src/link/http/fallback_http_config.dart'; -import 'package:graphql/src/link/http/http_config.dart'; -import './link_http_helper_deprecated_stub.dart' - if (dart.library.io) './link_http_helper_deprecated_io.dart'; - -class HttpLink extends Link { - HttpLink({ - @required String uri, - bool includeExtensions, - - /// pass on customized httpClient, especially handy for mocking and testing - Client httpClient, - Map headers, - Map credentials, - Map fetchOptions, - }) : super( - // @todo possibly this is a bug in dart analyzer - // ignore: undefined_named_parameter - request: ( - Operation operation, [ - NextLink forward, - ]) { - final parsedUri = Uri.parse(uri); - - if (operation.isSubscription) { - if (forward == null) { - throw Exception('This link does not support subscriptions.'); - } - return forward(operation); - } - - final Client fetcher = httpClient ?? Client(); - - final HttpConfig linkConfig = HttpConfig( - http: HttpQueryOptions( - includeExtensions: includeExtensions, - ), - options: fetchOptions, - credentials: credentials, - headers: headers, - ); - - final Map context = operation.getContext(); - HttpConfig contextConfig; - - if (context != null) { - // TODO: refactor context to use a [HttpConfig] object to avoid dynamic types - contextConfig = HttpConfig( - http: HttpQueryOptions( - includeExtensions: context['includeExtensions'] as bool, - ), - options: context['fetchOptions'] as Map, - credentials: context['credentials'] as Map, - headers: context['headers'] as Map, - ); - } - - final HttpHeadersAndBody httpHeadersAndBody = - _selectHttpOptionsAndBody( - operation, - fallbackHttpConfig, - linkConfig, - contextConfig, - ); - - final Map httpHeaders = httpHeadersAndBody.headers; - - StreamController controller; - - Future onListen() async { - StreamedResponse response; - - try { - // httpOptionsAndBody.body as String - final BaseRequest request = await _prepareRequest( - parsedUri, httpHeadersAndBody.body, httpHeaders); - - response = await fetcher.send(request); - - operation.setContext({ - 'response': response, - }); - final FetchResult parsedResponse = - await _parseResponse(response); - - controller.add(parsedResponse); - } catch (failure) { - // we overwrite socket uri for now: - // https://github.com/dart-lang/sdk/issues/12693 - dynamic translated = ex.translateFailure(failure); - if (translated is ex.NetworkException) { - translated.uri = parsedUri; - } - controller.addError(translated); - } - - await controller.close(); - } - - controller = StreamController(onListen: onListen); - - return controller.stream; - }, - ); -} - -Future> _getFileMap( - dynamic body, { - Map currentMap, - List currentPath = const [], -}) async { - currentMap ??= {}; - if (body is Map) { - final Iterable> entries = body.entries; - for (MapEntry element in entries) { - currentMap.addAll(await _getFileMap( - element.value, - currentMap: currentMap, - currentPath: List.from(currentPath)..add(element.key), - )); - } - return currentMap; - } - if (body is List) { - for (int i = 0; i < body.length; i++) { - currentMap.addAll(await _getFileMap( - body[i], - currentMap: currentMap, - currentPath: List.from(currentPath)..add(i.toString()), - )); - } - return currentMap; - } - if (body is MultipartFile) { - return currentMap - ..addAll({currentPath.join('.'): body}); - } - - // @deprecated, backward compatible only - // in case the body is io.File - // in future release, io.File will no longer be supported - if (isIoFile(body)) { - return deprecatedHelper(body, currentMap, currentPath); - } - - // else should only be either String, num, null; NOTHING else - return currentMap; -} - -Future _prepareRequest( - Uri uri, - Map body, - Map httpHeaders, -) async { - final Map fileMap = await _getFileMap(body); - if (fileMap.isEmpty) { - final Request r = Request('post', uri); - r.headers.addAll(httpHeaders); - r.body = json.encode(body); - return r; - } - - final MultipartRequest r = MultipartRequest('post', uri); - r.headers.addAll(httpHeaders); - r.fields['operations'] = json.encode(body, toEncodable: (dynamic object) { - if (object is MultipartFile) { - return null; - } - // @deprecated, backward compatible only - // in case the body is io.File - // in future release, io.File will no longer be supported - if (isIoFile(object)) { - return null; - } - return object.toJson(); - }); - - final Map> fileMapping = >{}; - final List fileList = []; - - final List> fileMapEntries = - fileMap.entries.toList(growable: false); - - for (int i = 0; i < fileMapEntries.length; i++) { - final MapEntry entry = fileMapEntries[i]; - final String indexString = i.toString(); - fileMapping.addAll(>{ - indexString: [entry.key], - }); - final MultipartFile f = entry.value; - fileList.add(MultipartFile( - indexString, - f.finalize(), - f.length, - contentType: f.contentType, - filename: f.filename, - )); - } - - r.fields['map'] = json.encode(fileMapping); - - r.files.addAll(fileList); - return r; -} - -HttpHeadersAndBody _selectHttpOptionsAndBody( - Operation operation, - HttpConfig fallbackConfig, [ - HttpConfig linkConfig, - HttpConfig contextConfig, -]) { - final Map options = { - 'headers': {}, - 'credentials': {}, - }; - final HttpQueryOptions http = HttpQueryOptions(); - - // http options - - // initialize with fallback http options - http.addAll(fallbackConfig.http); - - // inject the configured http options - if (linkConfig.http != null) { - http.addAll(linkConfig.http); - } - - // override with context http options - if (contextConfig.http != null) { - http.addAll(contextConfig.http); - } - - // options - - // initialize with fallback options - options.addAll(fallbackConfig.options); - - // inject the configured options - if (linkConfig.options != null) { - options.addAll(linkConfig.options); - } - - // override with context options - if (contextConfig.options != null) { - options.addAll(contextConfig.options); - } - - // headers - - // initialze with fallback headers - options['headers'].addAll(fallbackConfig.headers); - - // inject the configured headers - if (linkConfig.headers != null) { - options['headers'].addAll(linkConfig.headers); - } - - // inject the context headers - if (contextConfig.headers != null) { - options['headers'].addAll(contextConfig.headers); - } - - // credentials - - // initialze with fallback credentials - options['credentials'].addAll(fallbackConfig.credentials); - - // inject the configured credentials - if (linkConfig.credentials != null) { - options['credentials'].addAll(linkConfig.credentials); - } - - // inject the context credentials - if (contextConfig.credentials != null) { - options['credentials'].addAll(contextConfig.credentials); - } - - // the body depends on the http options - final Map body = { - 'operationName': operation.operationName, - 'variables': operation.variables, - }; - - // not sending the query (i.e persisted queries) - if (http.includeExtensions) { - body['extensions'] = operation.extensions; - } - - if (http.includeQuery) { - body['query'] = printNode(operation.documentNode); - } - - return HttpHeadersAndBody( - headers: options['headers'] as Map, - body: body, - ); -} - -Future _parseResponse(StreamedResponse response) async { - final int statusCode = response.statusCode; - - final Encoding encoding = _determineEncodingFromResponse(response); - // @todo limit bodyBytes - final Uint8List responseByte = await response.stream.toBytes(); - final String decodedBody = encoding.decode(responseByte); - - Map jsonResponse; - try { - jsonResponse= json.decode(decodedBody) as Map; - }catch(e){ - throw ClientException('Invalid response body: $decodedBody'); - } - final FetchResult fetchResult = FetchResult(); - - if (jsonResponse['errors'] != null) { - fetchResult.errors = - (jsonResponse['errors'] as List).where(notNull).toList(); - } - - if (jsonResponse['data'] != null) { - fetchResult.data = jsonResponse['data']; - } - - if (fetchResult.data == null && fetchResult.errors == null) { - if (statusCode < 200 || statusCode >= 400) { - throw ClientException( - 'Network Error: $statusCode $decodedBody', - ); - } - throw ClientException('Invalid response body: $decodedBody'); - } - - return fetchResult; -} - -/// Returns the charset encoding for the given response. -/// -/// The default fallback encoding is set to UTF-8 according to the IETF RFC4627 standard -/// which specifies the application/json media type: -/// "JSON text SHALL be encoded in Unicode. The default encoding is UTF-8." -Encoding _determineEncodingFromResponse(BaseResponse response, - [Encoding fallback = utf8]) { - final String contentType = response.headers['content-type']; - - if (contentType == null) { - return fallback; - } - - final MediaType mediaType = MediaType.parse(contentType); - final String charset = mediaType.parameters['charset']; - - if (charset == null) { - return fallback; - } - - final Encoding encoding = Encoding.getByName(charset); - - return encoding == null ? fallback : encoding; -} diff --git a/packages/graphql/lib/src/link/http/link_http_helper_deprecated_io.dart b/packages/graphql/lib/src/link/http/link_http_helper_deprecated_io.dart deleted file mode 100644 index 8288e86bc..000000000 --- a/packages/graphql/lib/src/link/http/link_http_helper_deprecated_io.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:io' as io; - -import 'package:http/http.dart'; -import 'package:graphql/src/utilities/file_io.dart' show multipartFileFrom; - -// @deprecated, backward compatible only -// in case the body is io.File -// in future release, io.File will no longer be supported -Future> deprecatedHelper( - body, currentMap, currentPath) async { - if (body is io.File) { - return currentMap - ..addAll({ - currentPath.join('.'): await multipartFileFrom(body) - }); - } - return null; -} - -bool isIoFile(object) { - final r = object is io.File; - if (r) { - print(r''' -⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️ DEPRECATION WARNING ⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️ - -Please do not use `File` direcly anymore. Instead, use -`MultipartFile`. There's also a utitlity method to help you -`import 'package:graphql/utilities.dart' show multipartFileFrom;` - -⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️ DEPRECATION WARNING ⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️ - '''); - } - return r; -} diff --git a/packages/graphql/lib/src/link/http/link_http_helper_deprecated_stub.dart b/packages/graphql/lib/src/link/http/link_http_helper_deprecated_stub.dart deleted file mode 100644 index 23c32aac8..000000000 --- a/packages/graphql/lib/src/link/http/link_http_helper_deprecated_stub.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:http/http.dart'; - -// @deprecated, backward compatible only -// in case the body is io.File -// in future release, io.File will no longer be supported -// but this stub is noop -Future> deprecatedHelper( - body, currentMap, currentPath) async => - null; - -// @deprecated, backward compatible only -// in case the body is io.File -// in future release, io.File will no longer be supported -// but this stub always returns false -bool isIoFile(object) => false; diff --git a/packages/graphql/lib/src/link/link.dart b/packages/graphql/lib/src/link/link.dart deleted file mode 100644 index d51d5a82b..000000000 --- a/packages/graphql/lib/src/link/link.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:async'; - -import 'package:graphql/src/link/fetch_result.dart'; -import 'package:graphql/src/link/operation.dart'; - -typedef NextLink = Stream Function( - Operation operation, -); - -typedef RequestHandler = Stream Function( - Operation operation, [ - NextLink forward, -]); - -Link _concat( - Link first, - Link second, -) { - return Link(request: ( - Operation operation, [ - NextLink forward, - ]) { - return first.request(operation, (Operation op) { - return second.request(op, forward); - }); - }); -} - -class Link { - Link({this.request}); - - RequestHandler request; - - static Link from(List links) { - assert(links.isNotEmpty); - return links.reduce((first, second) => first.concat(second)); - } - - Link concat(Link next) => _concat(this, next); -} - -Stream execute({Link link, Operation operation}) => - link.request(operation); diff --git a/packages/graphql/lib/src/link/operation.dart b/packages/graphql/lib/src/link/operation.dart deleted file mode 100644 index b8bcd1c51..000000000 --- a/packages/graphql/lib/src/link/operation.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:gql/ast.dart'; - -import 'package:graphql/src/core/raw_operation_data.dart'; -import 'package:graphql/src/utilities/get_from_ast.dart'; - -class Operation extends RawOperationData { - Operation({ - @Deprecated('The "document" option has been deprecated, use "documentNode" instead') - String document, - DocumentNode documentNode, - Map variables, - this.extensions, - String operationName, - }) : super( - // ignore: deprecated_member_use_from_same_package - document: document, - documentNode: documentNode, - variables: variables, - operationName: operationName); - - factory Operation.fromOptions(RawOperationData options) { - return Operation( - documentNode: options.documentNode, - variables: options.variables, - ); - } - - final Map extensions; - - final Map _context = {}; - - /// Sets the context of an operation by merging the new context with the old one. - void setContext(Map next) { - if (next != null) { - _context.addAll(next); - } - } - - Map getContext() { - final Map result = {}; - result.addAll(_context); - - return result; - } - - bool get isSubscription => isOfType( - OperationType.subscription, - documentNode, - operationName, - ); -} diff --git a/packages/graphql/lib/src/link/web_socket/link_web_socket.dart b/packages/graphql/lib/src/link/web_socket/link_web_socket.dart deleted file mode 100644 index acfd0fd46..000000000 --- a/packages/graphql/lib/src/link/web_socket/link_web_socket.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:meta/meta.dart'; - -import 'package:graphql/src/link/fetch_result.dart'; -import 'package:graphql/src/link/link.dart'; -import 'package:graphql/src/link/operation.dart'; -import 'package:graphql/src/socket_client.dart'; -import 'package:graphql/src/websocket/messages.dart'; - -/// A Universal Websocket [Link] implementation to support the websocket transport. -/// It supports subscriptions, query and mutation operations as well. -/// -/// NOTE: the actual socket connection will only get established after an [Operation] is handled by this [WebSocketLink]. -/// If you'd like to connect to the socket server instantly, call the [connectOrReconnect] method after creating this [WebSocketLink] instance. -class WebSocketLink extends Link { - /// Creates a new [WebSocketLink] instance with the specified config. - WebSocketLink({ - @required this.url, - this.config = const SocketClientConfig(), - }) : super() { - request = _doOperation; - } - - final String url; - final SocketClientConfig config; - - // cannot be final because we're changing the instance upon a header change. - SocketClient _socketClient; - - Stream _doOperation(Operation operation, [NextLink forward]) { - if (_socketClient == null) { - connectOrReconnect(); - } - - return _socketClient.subscribe(SubscriptionRequest(operation), true).map( - (SubscriptionData result) => FetchResult( - data: result.data, - errors: result.errors as List, - context: operation.getContext(), - extensions: operation.extensions), - ); - } - - /// Connects or reconnects to the server with the specified headers. - void connectOrReconnect() { - _socketClient?.dispose(); - _socketClient = SocketClient(url, config: config); - } - - /// Disposes the underlying socket client explicitly. Only use this, if you want to disconnect from - /// the current server in favour of another one. If that's the case, create a new [WebSocketLink] instance. - Future dispose() async { - await _socketClient?.dispose(); - _socketClient = null; - } -} diff --git a/packages/graphql/lib/src/socket_client.dart b/packages/graphql/lib/src/socket_client.dart deleted file mode 100644 index 306257903..000000000 --- a/packages/graphql/lib/src/socket_client.dart +++ /dev/null @@ -1,383 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:meta/meta.dart'; -import 'package:websocket/websocket.dart' show WebSocket, WebSocketStatus; - -import 'package:rxdart/rxdart.dart'; -import 'package:uuid_enhanced/uuid.dart'; - -import 'package:graphql/src/websocket/messages.dart'; - -typedef GetInitPayload = FutureOr Function(); - -class SocketClientConfig { - const SocketClientConfig({ - this.autoReconnect = true, - this.queryAndMutationTimeout = const Duration(seconds: 10), - this.inactivityTimeout = const Duration(seconds: 30), - this.delayBetweenReconnectionAttempts = const Duration(seconds: 5), - this.initPayload, - }); - - /// Whether to reconnect to the server after detecting connection loss. - final bool autoReconnect; - - /// The duration after which the connection is considered unstable, because no keep alive message - /// was received from the server in the given time-frame. The connection to the server will be closed. - /// If [autoReconnect] is set to true, we try to reconnect to the server after the specified [delayBetweenReconnectionAttempts]. - /// - /// If null, the keep alive messages will be ignored. - final Duration inactivityTimeout; - - /// The duration that needs to pass before trying to reconnect to the server after a connection loss. - /// This only takes effect when [autoReconnect] is set to true. - /// - /// If null, the reconnection will occur immediately, although not recommended. - final Duration delayBetweenReconnectionAttempts; - - // The duration after which a query or mutation should time out. - // If null, no timeout is applied, although not recommended. - final Duration queryAndMutationTimeout; - - /// The initial payload that will be sent to the server upon connection. - /// Can be null, but must be a valid json structure if provided. - final GetInitPayload initPayload; - - Future get initOperation async { - if (initPayload != null) { - dynamic payload = await initPayload(); - return InitOperation(payload); - } - - return InitOperation(null); - } -} - -enum SocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED } - -/// Wraps a standard web socket instance to marshal and un-marshal the server / -/// client payloads into dart object representation. -/// -/// This class also deals with reconnection, handles timeout and keep alive messages. -/// -/// It is meant to be instantiated once, and you can let this class handle all the heavy- -/// lifting of socket state management. Once you're done with the socket connection, make sure -/// you call the [dispose] method to release all allocated resources. -class SocketClient { - SocketClient( - this.url, { - this.protocols = const [ - 'graphql-ws', - ], - this.config = const SocketClientConfig(), - @visibleForTesting this.randomBytesForUuid, - }) { - _connect(); - } - - Uint8List randomBytesForUuid; - final String url; - final SocketClientConfig config; - final Iterable protocols; - final BehaviorSubject _connectionStateController = - BehaviorSubject(); - - final HashMap _subscriptionInitializers = HashMap(); - bool _connectionWasLost = false; - - Timer _reconnectTimer; - WebSocket _socket; - - @visibleForTesting - WebSocket get socket => _socket; - Stream _messageStream; - - StreamSubscription _keepAliveSubscription; - StreamSubscription _messageSubscription; - - /// Connects to the server. - /// - /// If this instance is disposed, this method does nothing. - Future _connect() async { - final InitOperation initOperation = await config.initOperation; - - if (_connectionStateController.isClosed) { - return; - } - - _connectionStateController.value = SocketConnectionState.CONNECTING; - print('Connecting to websocket: $url...'); - - try { - _socket = await WebSocket.connect( - url, - protocols: protocols, - ); - _connectionStateController.value = SocketConnectionState.CONNECTED; - print('Connected to websocket.'); - _write(initOperation); - - _messageStream = - _socket.stream.map(_parseSocketMessage); - - if (config.inactivityTimeout != null) { - _keepAliveSubscription = _messagesOfType().timeout( - config.inactivityTimeout, - onTimeout: (EventSink event) { - print( - "Haven't received keep alive message for ${config.inactivityTimeout.inSeconds} seconds. Disconnecting.."); - event.close(); - _socket.close(WebSocketStatus.goingAway); - _connectionStateController.value = - SocketConnectionState.NOT_CONNECTED; - }, - ).listen(null); - } - - _messageSubscription = _messageStream.listen( - (dynamic data) { - // print('data: $data'); - }, - onDone: () { - // print('done'); - onConnectionLost(); - }, - cancelOnError: true, - onError: (dynamic e) { - print('error: $e'); - }); - - if (_connectionWasLost) { - for (Function callback in _subscriptionInitializers.values) { - callback(); - } - - _connectionWasLost = false; - } - } catch (e) { - onConnectionLost(e); - } - } - - void onConnectionLost([e]) { - if (e != null) { - print('There was an error causing connection lost: $e'); - } - print('Disconnected from websocket.'); - _reconnectTimer?.cancel(); - _keepAliveSubscription?.cancel(); - _messageSubscription?.cancel(); - - if (_connectionStateController.isClosed) { - return; - } - - _connectionWasLost = true; - - if (_connectionStateController.value != - SocketConnectionState.NOT_CONNECTED) { - _connectionStateController.value = SocketConnectionState.NOT_CONNECTED; - } - - if (config.autoReconnect && !_connectionStateController.isClosed) { - if (config.delayBetweenReconnectionAttempts != null) { - print( - 'Scheduling to connect in ${config.delayBetweenReconnectionAttempts.inSeconds} seconds...'); - - _reconnectTimer = Timer( - config.delayBetweenReconnectionAttempts, - () { - _connect(); - }, - ); - } else { - Timer.run(() => _connect()); - } - } - } - - /// Closes the underlying socket if connected, and stops reconnection attempts. - /// After calling this method, this [SocketClient] instance must be considered - /// unusable. Instead, create a new instance of this class. - /// - /// Use this method if you'd like to disconnect from the specified server permanently, - /// and you'd like to connect to another server instead of the current one. - Future dispose() async { - print('Disposing socket client..'); - _reconnectTimer?.cancel(); - await Future.wait([ - _socket?.close(), - _keepAliveSubscription?.cancel(), - _messageSubscription?.cancel(), - _connectionStateController?.close(), - ]); - } - - static GraphQLSocketMessage _parseSocketMessage(dynamic message) { - final Map map = - json.decode(message as String) as Map; - final String type = (map['type'] ?? 'unknown') as String; - final dynamic payload = map['payload'] ?? {}; - final String id = (map['id'] ?? 'none') as String; - - switch (type) { - case MessageTypes.GQL_CONNECTION_ACK: - return ConnectionAck(); - case MessageTypes.GQL_CONNECTION_ERROR: - return ConnectionError(payload); - case MessageTypes.GQL_CONNECTION_KEEP_ALIVE: - return ConnectionKeepAlive(); - case MessageTypes.GQL_DATA: - final dynamic data = payload['data']; - final dynamic errors = payload['errors']; - return SubscriptionData(id, data, errors); - case MessageTypes.GQL_ERROR: - return SubscriptionError(id, payload); - case MessageTypes.GQL_COMPLETE: - return SubscriptionComplete(id); - default: - return UnknownData(map); - } - } - - void _write(final GraphQLSocketMessage message) { - if (_connectionStateController.value == SocketConnectionState.CONNECTED) { - _socket.add( - json.encode( - message, - toEncodable: (dynamic m) => m.toJson(), - ), - ); - } - } - - /// Sends a query, mutation or subscription request to the server, and returns a stream of the response. - /// - /// If the request is a query or mutation, a timeout will be applied to the request as specified by - /// [SocketClientConfig]'s [queryAndMutationTimeout] field. - /// - /// If the request is a subscription, obviously no timeout is applied. - /// - /// In case of socket disconnection, the returned stream will be closed. - Stream subscribe( - final SubscriptionRequest payload, final bool waitForConnection) { - final String id = Uuid.randomUuid(random: randomBytesForUuid).toString(); - final StreamController response = - StreamController(); - StreamSubscription sub; - final bool addTimeout = !payload.operation.isSubscription && - config.queryAndMutationTimeout != null; - - final onListen = () { - final Stream waitForConnectedStateWithoutTimeout = - _connectionStateController - .startWith( - waitForConnection ? null : SocketConnectionState.CONNECTED) - .where((SocketConnectionState state) => - state == SocketConnectionState.CONNECTED) - .take(1); - - final Stream waitForConnectedState = addTimeout - ? waitForConnectedStateWithoutTimeout.timeout( - config.queryAndMutationTimeout, - onTimeout: (EventSink event) { - print('Connection timed out.'); - response.addError(TimeoutException('Connection timed out.')); - event.close(); - response.close(); - }, - ) - : waitForConnectedStateWithoutTimeout; - - sub = waitForConnectedState.listen((_) { - final Stream dataErrorComplete = - _messageStream.where( - (GraphQLSocketMessage message) { - if (message is SubscriptionData) { - return message.id == id; - } - - if (message is SubscriptionError) { - return message.id == id; - } - - if (message is SubscriptionComplete) { - return message.id == id; - } - - return false; - }, - ).takeWhile((_) => !response.isClosed); - - final Stream subscriptionComplete = addTimeout - ? dataErrorComplete - .where((GraphQLSocketMessage message) => - message is SubscriptionComplete) - .take(1) - .timeout( - config.queryAndMutationTimeout, - onTimeout: (EventSink event) { - print('Request timed out.'); - response.addError(TimeoutException('Request timed out.')); - event.close(); - response.close(); - }, - ) - : dataErrorComplete - .where((GraphQLSocketMessage message) => - message is SubscriptionComplete) - .take(1); - - subscriptionComplete.listen((_) => response.close()); - - dataErrorComplete - .where( - (GraphQLSocketMessage message) => message is SubscriptionData) - .cast() - .listen((SubscriptionData message) => response.add(message)); - - dataErrorComplete - .where( - (GraphQLSocketMessage message) => message is SubscriptionError) - .listen( - (GraphQLSocketMessage message) => response.addError(message)); - - _write(StartOperation(id, payload)); - }); - }; - - response.onListen = onListen; - - response.onCancel = () { - _subscriptionInitializers.remove(id); - - sub?.cancel(); - if (_connectionStateController.value == SocketConnectionState.CONNECTED && - _socket != null) { - _write(StopOperation(id)); - } - }; - - _subscriptionInitializers[id] = onListen; - - return response.stream; - } - - /// These streams will emit done events when the current socket is done. - - /// A stream that emits the last value of the connection state upon subscription. - Stream get connectionState => - _connectionStateController.stream; - - /// Filter `_messageStream` for messages of the given type of [GraphQLSocketMessage] - /// - /// Example usages: - /// `_messagesOfType()` for init acknowledgments - /// `_messagesOfType()` for errors - /// `_messagesOfType()` for unknown data messages - Stream _messagesOfType() => _messageStream - .where((GraphQLSocketMessage message) => message is M) - .cast(); -} diff --git a/packages/graphql/lib/src/utilities/file.dart b/packages/graphql/lib/src/utilities/file.dart deleted file mode 100644 index 1ebf5ff6c..000000000 --- a/packages/graphql/lib/src/utilities/file.dart +++ /dev/null @@ -1,3 +0,0 @@ -export './file_stub.dart' - if (dart.library.html) './file_html.dart' - if (dart.library.io) './file_io.dart'; diff --git a/packages/graphql/lib/src/utilities/file_html.dart b/packages/graphql/lib/src/utilities/file_html.dart deleted file mode 100644 index c10ea16bf..000000000 --- a/packages/graphql/lib/src/utilities/file_html.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:async'; -import 'dart:html' as html; -import 'package:http/http.dart'; -import 'package:http_parser/http_parser.dart'; - -Stream> _readFile(html.File file) { - final reader = html.FileReader(); - final streamController = StreamController>(); - - reader.onLoad.listen((_) { - // streamController.add(reader.result); - streamController.close(); - }); - - reader.onError.listen((error) => streamController.addError(error)); - - reader.readAsArrayBuffer(file); - - return streamController.stream; -} - -MultipartFile multipartFileFrom(html.File f) => MultipartFile( - '', - _readFile(f), - f.size, - contentType: MediaType.parse(f.type), - filename: f.name, - ); diff --git a/packages/graphql/lib/src/utilities/file_io.dart b/packages/graphql/lib/src/utilities/file_io.dart deleted file mode 100644 index 0ee0a00f2..000000000 --- a/packages/graphql/lib/src/utilities/file_io.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'dart:async'; -import 'dart:io' as io; -import 'package:http/http.dart'; -import 'package:http_parser/http_parser.dart'; -import 'package:mime/mime.dart'; -import 'package:path/path.dart'; - -MediaType contentType(f) { - final a = lookupMimeType(f.path); - if (a == null) { - return null; - } - final b = MediaType.parse(a); - return b; -} - -Future multipartFileFrom(io.File f) async => MultipartFile( - '', - f.openRead(), - await f.length(), - contentType: contentType(f), - filename: basename(f.path), - ); diff --git a/packages/graphql/lib/src/utilities/file_stub.dart b/packages/graphql/lib/src/utilities/file_stub.dart deleted file mode 100644 index f9ea1c01c..000000000 --- a/packages/graphql/lib/src/utilities/file_stub.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'dart:async'; - -import 'package:http/http.dart'; - -FutureOr multipartFileFrom(/*io.File or html.File*/ f) => - throw UnsupportedError('io or html'); diff --git a/packages/graphql/lib/src/websocket/messages.dart b/packages/graphql/lib/src/websocket/messages.dart deleted file mode 100644 index c426b78ae..000000000 --- a/packages/graphql/lib/src/websocket/messages.dart +++ /dev/null @@ -1,224 +0,0 @@ -import 'dart:convert'; - -import 'package:gql/language.dart'; -import 'package:graphql/src/link/operation.dart'; - -/// These messages represent the structures used for Client-server communication -/// in a GraphQL web-socket subscription. Each message is represented in a JSON -/// format where the data type is denoted by the `type` field. - -/// A list of constants used for identifying message types -class MessageTypes { - MessageTypes._(); - - // client connections - static const String GQL_CONNECTION_INIT = 'connection_init'; - static const String GQL_CONNECTION_TERMINATE = 'connection_terminate'; - - // server connections - static const String GQL_CONNECTION_ACK = 'connection_ack'; - static const String GQL_CONNECTION_ERROR = 'connection_error'; - static const String GQL_CONNECTION_KEEP_ALIVE = 'ka'; - - // client operations - static const String GQL_START = 'start'; - static const String GQL_STOP = 'stop'; - - // server operations - static const String GQL_DATA = 'data'; - static const String GQL_ERROR = 'error'; - static const String GQL_COMPLETE = 'complete'; - - // default tag for use in identifying issues - static const String GQL_UNKNOWN = 'unknown'; -} - -abstract class JsonSerializable { - Map toJson(); - - @override - String toString() => toJson().toString(); -} - -/// Base type for representing a server-client subscription message. -abstract class GraphQLSocketMessage extends JsonSerializable { - GraphQLSocketMessage(this.type); - - final String type; -} - -/// After establishing a connection with the server, the client will -/// send this message to tell the server that it is ready to begin sending -/// new subscription queries. -class InitOperation extends GraphQLSocketMessage { - InitOperation(this.payload) : super(MessageTypes.GQL_CONNECTION_INIT); - - final dynamic payload; - - @override - Map toJson() { - final Map jsonMap = {}; - jsonMap['type'] = type; - - if (payload != null) { - jsonMap['payload'] = payload; - } - - return jsonMap; - } -} - -/// Represent the payload used during a Start query operation. -/// The operationName should match one of the top level query definitions -/// defined in the query provided. Additional variables can be provided -/// and sent to the server for processing. -class SubscriptionRequest extends JsonSerializable { - SubscriptionRequest(this.operation); - final Operation operation; - - @override - Map toJson() => { - 'operationName': operation.operationName, - 'query': printNode(operation.documentNode), - 'variables': operation.variables, - }; -} - -/// A message to tell the server to create a subscription. The contents of the -/// query will be defined by the payload request. The id provided will be used -/// to tag messages such that they can be identified for this subscription -/// instance. id values should be unique and not be re-used during the lifetime -/// of the server. -class StartOperation extends GraphQLSocketMessage { - StartOperation(this.id, this.payload) : super(MessageTypes.GQL_START); - - final String id; - final SubscriptionRequest payload; - - @override - Map toJson() => { - 'type': type, - 'id': id, - 'payload': payload, - }; -} - -/// Tell the server to stop sending subscription data for a particular -/// subscription instance. See [StartOperation]. -class StopOperation extends GraphQLSocketMessage { - StopOperation(this.id) : super(MessageTypes.GQL_STOP); - - final String id; - - @override - Map toJson() => { - 'type': type, - 'id': id, - }; -} - -/// The server will send this acknowledgment message after receiving the init -/// command from the client if the init was successful. -class ConnectionAck extends GraphQLSocketMessage { - ConnectionAck() : super(MessageTypes.GQL_CONNECTION_ACK); - - @override - Map toJson() => { - 'type': type, - }; -} - -/// The server will send this error message after receiving the init command -/// from the client if the init was not successful. -class ConnectionError extends GraphQLSocketMessage { - ConnectionError(this.payload) : super(MessageTypes.GQL_CONNECTION_ERROR); - - final dynamic payload; - - @override - Map toJson() => { - 'type': type, - 'payload': payload, - }; -} - -/// The server will send this message to keep the connection alive -class ConnectionKeepAlive extends GraphQLSocketMessage { - ConnectionKeepAlive() : super(MessageTypes.GQL_CONNECTION_KEEP_ALIVE); - - @override - Map toJson() => { - 'type': type, - }; -} - -/// Data sent from the server to the client with subscription data or error -/// payload. The user should check the errors result before processing the -/// data value. These error are from the query resolvers. -class SubscriptionData extends GraphQLSocketMessage { - SubscriptionData(this.id, this.data, this.errors) - : super(MessageTypes.GQL_DATA); - - final String id; - final dynamic data; - final dynamic errors; - - @override - Map toJson() => { - 'type': type, - 'data': data, - 'errors': errors, - }; - - @override - int get hashCode => toJson().hashCode; - - @override - bool operator ==(dynamic other) => - other is SubscriptionData && jsonEncode(other) == jsonEncode(this); -} - -/// Errors sent from the server to the client if the subscription operation was -/// not successful, usually due to GraphQL validation errors. -class SubscriptionError extends GraphQLSocketMessage { - SubscriptionError(this.id, this.payload) : super(MessageTypes.GQL_ERROR); - - final String id; - final dynamic payload; - - @override - Map toJson() => { - 'type': type, - 'id': id, - 'payload': payload, - }; -} - -/// Server message to the client to indicate that no more data will be sent -/// for a particular subscription instance. -class SubscriptionComplete extends GraphQLSocketMessage { - SubscriptionComplete(this.id) : super(MessageTypes.GQL_COMPLETE); - - final String id; - - @override - Map toJson() => { - 'type': type, - 'id': id, - }; -} - -/// Not expected to be created. Indicates there are problems parsing the server -/// response, or that new unsupported types have been added to the subscription -/// implementation. -class UnknownData extends GraphQLSocketMessage { - UnknownData(this.payload) : super(MessageTypes.GQL_UNKNOWN); - - final dynamic payload; - - @override - Map toJson() => { - 'type': type, - 'payload': payload, - }; -} diff --git a/packages/graphql/lib/utilities.dart b/packages/graphql/lib/utilities.dart deleted file mode 100644 index dab0e3aff..000000000 --- a/packages/graphql/lib/utilities.dart +++ /dev/null @@ -1 +0,0 @@ -export 'package:graphql/src/utilities/file.dart' show multipartFileFrom; diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 73c069193..c0b3cf979 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -10,14 +10,10 @@ authors: homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 - http: ^0.12.0+4 - mime: ^0.9.6+2 path: ^1.6.2 - http_parser: ^3.1.3 - uuid_enhanced: ^3.0.2 gql: ^0.12.0 - rxdart: ^0.23.1 - websocket: ^0.0.5 + gql_exec: ^0.2.2 + gql_link: ^0.2.3 quiver: '>=2.0.0 <3.0.0' dev_dependencies: pedantic: ^1.8.0+1 diff --git a/packages/graphql/test/anonymous_operations_test.dart b/packages/graphql/test/anonymous_operations_test.dart index ea65d3e36..3dbcdca6f 100644 --- a/packages/graphql/test/anonymous_operations_test.dart +++ b/packages/graphql/test/anonymous_operations_test.dart @@ -1,13 +1,14 @@ import 'package:gql/language.dart'; +import 'package:gql_exec/gql_exec.dart'; +import 'package:gql_link/gql_link.dart'; import 'package:test/test.dart'; import 'package:mockito/mockito.dart'; -import 'package:http/http.dart' as http; import 'package:graphql/client.dart'; import './helpers.dart'; -class MockHttpClient extends Mock implements http.Client {} +class MockLink extends Mock implements Link {} void main() { const String readRepositories = r'''{ @@ -33,86 +34,77 @@ void main() { } '''; - HttpLink httpLink; - AuthLink authLink; - Link link; + MockLink link; GraphQLClient graphQLClientClient; - MockHttpClient mockHttpClient; + group('simple json', () { setUp(() { - mockHttpClient = MockHttpClient(); - - httpLink = HttpLink( - uri: 'https://api.github.com/graphql', httpClient: mockHttpClient); - - authLink = AuthLink( - getToken: () async => 'Bearer my-special-bearer-token', - ); - - link = authLink.concat(httpLink); + link = MockLink(); graphQLClientClient = GraphQLClient( cache: getTestCache(), link: link, ); }); + group('query', () { test('successful query', () async { final WatchQueryOptions _options = WatchQueryOptions( documentNode: parseString(readRepositories), variables: {}, ); + when( - mockHttpClient.send(any), - ).thenAnswer((Invocation a) async { - return simpleResponse(body: r''' -{ - "data": { - "viewer": { - "repositories": { - "nodes": [ - { - "__typename": "Repository", - "id": "MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==", - "name": "pq", - "viewerHasStarred": false - }, - { - "__typename": "Repository", - "id": "MDEwOlJlcG9zaXRvcnkzMjkyNDQ0Mw==", - "name": "go-evercookie", - "viewerHasStarred": false - }, - { - "__typename": "Repository", - "id": "MDEwOlJlcG9zaXRvcnkzNTA0NjgyNA==", - "name": "watchbot", - "viewerHasStarred": false - } - ] - } - } - } -} - '''); - }); + link.request(any), + ).thenAnswer( + (_) => Stream.fromIterable( + [ + Response( + data: { + 'viewer': { + 'repositories': { + 'nodes': [ + { + '__typename': 'Repository', + 'id': 'MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==', + 'name': 'pq', + 'viewerHasStarred': false, + }, + { + '__typename': 'Repository', + 'id': 'MDEwOlJlcG9zaXRvcnkzMjkyNDQ0Mw==', + 'name': 'go-evercookie', + 'viewerHasStarred': false, + }, + { + '__typename': 'Repository', + 'id': 'MDEwOlJlcG9zaXRvcnkzNTA0NjgyNA==', + 'name': 'watchbot', + 'viewerHasStarred': false, + }, + ], + }, + }, + }, + ), + ], + ), + ); + final QueryResult r = await graphQLClientClient.query(_options); - final http.Request capt = verify(mockHttpClient.send(captureAny)) - .captured - .first as http.Request; - expect(capt.method, 'post'); - expect(capt.url.toString(), 'https://api.github.com/graphql'); - expect( - capt.headers, - { - 'accept': '*/*', - 'content-type': 'application/json; charset=utf-8', - 'Authorization': 'Bearer my-special-bearer-token', - }, + verify( + link.request( + Request( + operation: Operation( + document: parseString(readRepositories), + operationName: null, + ), + variables: {}, + context: Context(), + ), + ), ); - expect(await capt.finalize().bytesToString(), - r'{"operationName":null,"variables":{},"query":"query {\n viewer {\n repositories(last: 42) {\n nodes {\n __typename\n id\n name\n viewerHasStarred\n }\n }\n }\n}"}'); expect(r.exception, isNull); expect(r.data, isNotNull); @@ -136,30 +128,41 @@ void main() { }); group('mutation', () { test('successful mutation', () async { - final MutationOptions _options = - MutationOptions(documentNode: parseString(addStar)); - when(mockHttpClient.send(any)).thenAnswer((Invocation a) async => - simpleResponse( - body: - '{"data":{"action":{"starrable":{"viewerHasStarred":true}}}}')); + final MutationOptions _options = MutationOptions( + documentNode: parseString(addStar), + ); + + when( + link.request(any), + ).thenAnswer( + (_) => Stream.fromIterable( + [ + Response( + data: { + 'action': { + 'starrable': { + 'viewerHasStarred': true, + }, + }, + }, + ), + ], + ), + ); final QueryResult response = await graphQLClientClient.mutate(_options); - final http.Request request = verify(mockHttpClient.send(captureAny)) - .captured - .first as http.Request; - expect(request.method, 'post'); - expect(request.url.toString(), 'https://api.github.com/graphql'); - expect( - request.headers, - { - 'accept': '*/*', - 'content-type': 'application/json; charset=utf-8', - 'Authorization': 'Bearer my-special-bearer-token', - }, + verify( + link.request( + Request( + operation: Operation( + document: parseString(addStar), + ), + variables: {}, + context: Context(), + ), + ), ); - expect(await request.finalize().bytesToString(), - r'{"operationName":null,"variables":{},"query":"mutation {\n action: addStar(input: {starrableId: \"some_repo\"}) {\n starrable {\n viewerHasStarred\n }\n }\n}"}'); expect(response.exception, isNull); expect(response.data, isNotNull); diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index 19d67f1f2..19e983f4e 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -1,13 +1,14 @@ +import 'package:gql_exec/gql_exec.dart'; +import 'package:gql_link/gql_link.dart'; import 'package:test/test.dart'; import 'package:mockito/mockito.dart'; -import 'package:http/http.dart' as http; import 'package:graphql/client.dart'; import 'package:gql/language.dart'; import './helpers.dart'; -class MockHttpClient extends Mock implements http.Client {} +class MockLink extends Mock implements Link {} void main() { const String readRepositories = r''' @@ -35,88 +36,81 @@ void main() { } '''; - HttpLink httpLink; - AuthLink authLink; - Link link; + MockLink link; GraphQLClient graphQLClientClient; - MockHttpClient mockHttpClient; + group('simple json', () { setUp(() { - mockHttpClient = MockHttpClient(); - - httpLink = HttpLink( - uri: 'https://api.github.com/graphql', httpClient: mockHttpClient); - - authLink = AuthLink( - getToken: () async => 'Bearer my-special-bearer-token', - ); - - link = authLink.concat(httpLink); + link = MockLink(); graphQLClientClient = GraphQLClient( cache: getTestCache(), link: link, ); }); + group('query', () { - test('successful query', () async { + test('successful response', () async { final WatchQueryOptions _options = WatchQueryOptions( documentNode: parseString(readRepositories), variables: { 'nRepositories': 42, }, ); + when( - mockHttpClient.send(any), - ).thenAnswer((Invocation a) async { - return simpleResponse(body: r''' -{ - "data": { - "viewer": { - "repositories": { - "nodes": [ - { - "__typename": "Repository", - "id": "MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==", - "name": "pq", - "viewerHasStarred": false - }, - { - "__typename": "Repository", - "id": "MDEwOlJlcG9zaXRvcnkzMjkyNDQ0Mw==", - "name": "go-evercookie", - "viewerHasStarred": false - }, - { - "__typename": "Repository", - "id": "MDEwOlJlcG9zaXRvcnkzNTA0NjgyNA==", - "name": "watchbot", - "viewerHasStarred": false - } - ] - } - } - } -} - '''); - }); + link.request(any), + ).thenAnswer( + (_) => Stream.fromIterable( + [ + Response( + data: { + 'viewer': { + 'repositories': { + 'nodes': [ + { + '__typename': 'Repository', + 'id': 'MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==', + 'name': 'pq', + 'viewerHasStarred': false, + }, + { + '__typename': 'Repository', + 'id': 'MDEwOlJlcG9zaXRvcnkzMjkyNDQ0Mw==', + 'name': 'go-evercookie', + 'viewerHasStarred': false, + }, + { + '__typename': 'Repository', + 'id': 'MDEwOlJlcG9zaXRvcnkzNTA0NjgyNA==', + 'name': 'watchbot', + 'viewerHasStarred': false, + }, + ], + }, + }, + }, + ), + ], + ), + ); + final QueryResult r = await graphQLClientClient.query(_options); - final http.Request capt = verify(mockHttpClient.send(captureAny)) - .captured - .first as http.Request; - expect(capt.method, 'post'); - expect(capt.url.toString(), 'https://api.github.com/graphql'); - expect( - capt.headers, - { - 'accept': '*/*', - 'content-type': 'application/json; charset=utf-8', - 'Authorization': 'Bearer my-special-bearer-token', - }, + verify( + link.request( + Request( + operation: Operation( + document: parseString(readRepositories), + operationName: 'ReadRepositories', + ), + variables: { + 'nRepositories': 42, + }, + context: Context(), + ), + ), ); - expect(await capt.finalize().bytesToString(), - r'{"operationName":"ReadRepositories","variables":{"nRepositories":42},"query":"query ReadRepositories($nRepositories: Int!) {\n viewer {\n repositories(last: $nRepositories) {\n nodes {\n __typename\n id\n name\n viewerHasStarred\n }\n }\n }\n}"}'); expect(r.exception, isNull); expect(r.data, isNotNull); @@ -132,15 +126,23 @@ void main() { test('failed query because of an exception with null string', () async { final e = Exception(); - when(mockHttpClient.send(any)).thenAnswer((_) async { - throw e; - }); + + when( + link.request(any), + ).thenAnswer( + (_) => Stream.fromFuture(Future.error(e)), + ); final QueryResult r = await graphQLClientClient.query( - WatchQueryOptions(documentNode: parseString(readRepositories))); + WatchQueryOptions( + documentNode: parseString(readRepositories), + ), + ); - expect((r.exception.clientException as UnhandledFailureWrapper).failure, - e); + expect( + (r.exception.clientException as UnhandledFailureWrapper).failure, + e, + ); return; }); @@ -148,12 +150,17 @@ void main() { test('failed query because of an exception with empty string', () async { final e = Exception(''); - when(mockHttpClient.send(any)).thenAnswer((_) async { - throw e; - }); + when( + link.request(any), + ).thenAnswer( + (_) => Stream.fromFuture(Future.error(e)), + ); final QueryResult r = await graphQLClientClient.query( - WatchQueryOptions(documentNode: parseString(readRepositories))); + WatchQueryOptions( + documentNode: parseString(readRepositories), + ), + ); expect( (r.exception.clientException as UnhandledFailureWrapper).failure, @@ -172,30 +179,42 @@ void main() { }); group('mutation', () { test('successful mutation', () async { - final MutationOptions _options = - MutationOptions(documentNode: parseString(addStar)); - when(mockHttpClient.send(any)).thenAnswer((Invocation a) async => - simpleResponse( - body: - '{"data":{"action":{"starrable":{"viewerHasStarred":true}}}}')); + final MutationOptions _options = MutationOptions( + documentNode: parseString(addStar), + ); + + when( + link.request(any), + ).thenAnswer( + (_) => Stream.fromIterable( + [ + Response( + data: { + 'action': { + 'starrable': { + 'viewerHasStarred': true, + }, + }, + }, + ), + ], + ), + ); final QueryResult response = await graphQLClientClient.mutate(_options); - final http.Request request = verify(mockHttpClient.send(captureAny)) - .captured - .first as http.Request; - expect(request.method, 'post'); - expect(request.url.toString(), 'https://api.github.com/graphql'); - expect( - request.headers, - { - 'accept': '*/*', - 'content-type': 'application/json; charset=utf-8', - 'Authorization': 'Bearer my-special-bearer-token', - }, + verify( + link.request( + Request( + operation: Operation( + document: parseString(addStar), + operationName: 'AddStar', + ), + variables: {}, + context: Context(), + ), + ), ); - expect(await request.finalize().bytesToString(), - r'{"operationName":"AddStar","variables":{},"query":"mutation AddStar($starrableId: ID!) {\n action: addStar(input: {starrableId: $starrableId}) {\n starrable {\n viewerHasStarred\n }\n }\n}"}'); expect(response.exception, isNull); expect(response.data, isNotNull); diff --git a/packages/graphql/test/helpers.dart b/packages/graphql/test/helpers.dart index 5c51940a5..04aeca0cb 100644 --- a/packages/graphql/test/helpers.dart +++ b/packages/graphql/test/helpers.dart @@ -1,8 +1,4 @@ import 'dart:async'; -import 'dart:convert'; - -import 'package:meta/meta.dart'; -import 'package:http/http.dart' as http; import 'package:graphql/client.dart'; @@ -17,13 +13,3 @@ overridePrint(testFn(List log)) => () { NormalizedInMemoryCache getTestCache() => NormalizedInMemoryCache( dataIdFromObject: typenameDataIdFromObject, ); - -http.StreamedResponse simpleResponse({@required String body, int status}) { - final List bytes = utf8.encode(body); - final Stream> stream = - Stream>.fromIterable(>[bytes]); - - final http.StreamedResponse r = http.StreamedResponse(stream, status ?? 200); - - return r; -} diff --git a/packages/graphql/test/link/error/link_error_test.dart b/packages/graphql/test/link/error/link_error_test.dart deleted file mode 100644 index bcf56b35e..000000000 --- a/packages/graphql/test/link/error/link_error_test.dart +++ /dev/null @@ -1,110 +0,0 @@ -import "dart:async"; -import "dart:convert"; - -import 'package:gql/language.dart'; -import 'package:graphql/src/exceptions/exceptions.dart'; -import 'package:graphql/src/link/error/link_error.dart'; -import 'package:graphql/src/link/http/link_http.dart'; -import 'package:graphql/src/link/link.dart'; -import 'package:graphql/src/link/operation.dart'; -import "package:http/http.dart" as http; -import "package:mockito/mockito.dart"; -import "package:test/test.dart"; - -class MockClient extends Mock implements http.Client {} - -void main() { - group('error link', () { - MockClient client; - Operation query; - HttpLink httpLink; - - setUp(() { - client = MockClient(); - query = Operation( - documentNode: parseString('query Operation {}'), - operationName: 'Operation', - ); - httpLink = HttpLink( - uri: '/graphql-test', - httpClient: client, - ); - }); - - test('network error', () async { - bool called = false; - - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{}')], - ), - 400, - ), - ), - ); - - final errorLink = ErrorLink(errorHandler: (response) { - if (response.exception.clientException != null) { - called = true; - } - }); - - Exception exception; - - try { - await execute( - link: errorLink.concat(httpLink), - operation: query, - ).first; - } on Exception catch (e) { - exception = e; - } - - expect( - exception, - const TypeMatcher(), - ); - expect( - called, - true, - ); - }); - - test('graphql error', () async { - bool called = false; - - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{"errors":[{"message":"error"}]}')], - ), - 200, - ), - ), - ); - - final errorLink = ErrorLink(errorHandler: (response) { - if (response.exception.graphqlErrors != null) { - called = true; - } - }); - - await execute( - link: errorLink.concat(httpLink), - operation: query, - ).first; - - expect( - called, - true, - ); - }); - }); -} diff --git a/packages/graphql/test/link/http/link_http_test.dart b/packages/graphql/test/link/http/link_http_test.dart deleted file mode 100644 index b6e35ba4a..000000000 --- a/packages/graphql/test/link/http/link_http_test.dart +++ /dev/null @@ -1,543 +0,0 @@ -import "dart:async"; -import "dart:convert"; - -import 'package:gql/language.dart'; -import 'package:graphql/client.dart'; -import 'package:graphql/internal.dart'; -import 'package:graphql/src/link/http/link_http.dart'; -import 'package:graphql/src/link/link.dart'; -import 'package:graphql/src/link/operation.dart'; -import "package:http/http.dart" as http; -import 'package:http_parser/http_parser.dart'; -import "package:mockito/mockito.dart"; -import "package:test/test.dart"; - -class MockClient extends Mock implements http.Client {} - -void main() { - group('HTTP link', () { - MockClient client; - Operation query; - Operation subscription; - HttpLink link; - - setUp(() { - client = MockClient(); - query = Operation( - documentNode: parseString('query Operation {}'), - operationName: 'Operation', - ); - subscription = Operation( - documentNode: parseString('subscription Operation {}'), - operationName: 'Operation', - ); - link = HttpLink( - uri: '/graphql-test', - httpClient: client, - ); - }); - - test('exception on subscription', () { - expect( - () => execute(link: link, operation: subscription), - throwsA( - const TypeMatcher(), - ), - ); - }); - - test('forward on subscription', () { - bool forwardCalled = false; - - final forwardLink = Link( - request: (Operation op, [NextLink forward]) { - forwardCalled = true; - - return null; - }, - ); - expect( - execute( - link: link.concat(forwardLink), - operation: subscription, - ), - null, - ); - - expect( - forwardCalled, - true, - ); - }); - - test('request', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{"data":{}}')], - ), - 200, - ), - ), - ); - - await execute( - link: link, - operation: query, - ).first; - - final http.Request captured = verify( - client.send(captureAny), - ).captured.single; - - expect( - captured.url, - Uri.parse('/graphql-test'), - ); - expect( - captured.method, - 'post', - ); - expect( - captured.headers, - equals({ - 'accept': '*/*', - 'content-type': 'application/json; charset=utf-8', - }), - ); - expect( - captured.body, - '{"operationName":"Operation","variables":{},"query":"query Operation {\\n \\n}"}', - ); - }); - - test('request with link defaults', () async { - link = HttpLink( - uri: '/graphql-test', - httpClient: client, - includeExtensions: true, - fetchOptions: {'option-1:default': 'option-value-1:default'}, - credentials: {'credential-1:default': 'credential-value-1:default'}, - headers: {'header-1:default': 'header-value-1:default'}, - ); - - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{"data":{}}')], - ), - 200, - ), - ), - ); - - await execute( - link: link, - operation: query, - ).first; - - final http.Request captured = verify( - client.send(captureAny), - ).captured.single; - - expect( - captured.url, - Uri.parse('/graphql-test'), - ); - expect( - captured.method, - 'post', - ); - expect( - captured.headers, - equals({ - 'accept': '*/*', - 'content-type': 'application/json; charset=utf-8', - 'header-1:default': 'header-value-1:default', - }), - ); - expect( - captured.body, - '{"operationName":"Operation","variables":{},"extensions":null,"query":"query Operation {\\n \\n}"}', - ); - }); - - test('request with context', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{"data":{}}')], - ), - 200, - ), - ), - ); - - query.setContext({ - 'includeExtensions': true, - 'fetchOptions': {'option-1': 'option-value-1'}, - 'credentials': {'credential-1': 'credential-value-1'}, - 'headers': {'header-1': 'header-value-1'}, - }); - - await execute( - link: link, - operation: query, - ).first; - - final http.Request captured = verify( - client.send(captureAny), - ).captured.single; - - expect( - captured.url, - Uri.parse('/graphql-test'), - ); - expect( - captured.method, - 'post', - ); - expect( - captured.headers, - equals({ - 'accept': '*/*', - 'content-type': 'application/json; charset=utf-8', - 'header-1': 'header-value-1', - }), - ); - expect( - captured.body, - '{"operationName":"Operation","variables":{},"extensions":null,"query":"query Operation {\\n \\n}"}', - ); - }); - - test('request with extensions', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{"data":{}}')], - ), - 200, - ), - ), - ); - - final query = Operation( - documentNode: parseString('{}'), - extensions: {'extension-1': 'extension-value-1'}, - ); - query.setContext({ - 'includeExtensions': true, - }); - - await execute( - link: link, - operation: query, - ).first; - - final http.Request captured = verify( - client.send(captureAny), - ).captured.single; - - expect( - captured.body, - '{"operationName":null,"variables":{},"extensions":{"extension-1":"extension-value-1"},"query":"query {\\n \\n}"}', - ); - }); - - test('successful data response', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{"data":{}}')], - ), - 200, - ), - ), - ); - - final result = await execute( - link: link, - operation: query, - ).first; - - expect( - result.data, - equals({}), - ); - expect( - result.errors, - null, - ); - }); - - test('successful error response', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{"errors":[]}')], - ), - 200, - ), - ), - ); - - final result = await execute( - link: link, - operation: query, - ).first; - - expect( - result.errors, - equals([]), - ); - expect( - result.data, - null, - ); - }); - - test('no data and errors suceessful response', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{}')], - ), - 200, - ), - ), - ); - - Exception exception; - - try { - await execute( - link: link, - operation: query, - ).first; - } on Exception catch (e) { - exception = e; - } - - expect( - exception, - const TypeMatcher(), - ); - - expect( - (exception as NetworkException).wrappedException, - const TypeMatcher(), - ); - - expect( - exception.toString(), - 'Failed to connect to /graphql-test: Invalid response body: {}', - ); - }); - - test('no data and errors failed response', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{}')], - ), - 400, - ), - ), - ); - - Exception exception; - - try { - await execute( - link: link, - operation: query, - ).first; - } on Exception catch (e) { - exception = e; - } - - expect( - exception, - const TypeMatcher(), - ); - - expect( - (exception as NetworkException).wrappedException, - const TypeMatcher(), - ); - - expect( - exception.toString(), - 'Failed to connect to /graphql-test: Network Error: 400 {}', - ); - }); - - test('data on failed response', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{"data":{}}')], - ), - 300, - ), - ), - ); - - final result = await execute( - link: link, - operation: query, - ).first; - - expect( - result.data, - equals({}), - ); - expect( - result.errors, - null, - ); - }); - - test('non-json response', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('')], - ), - 200, - ), - ), - ); - - Exception exception; - - try { - await execute( - link: link, - operation: query, - ).first; - } on Exception catch (e) { - exception = e; - } - - expect( - exception, - const TypeMatcher(), - ); - - expect( - (exception as ClientException).message, - "Invalid response body: ", - ); - }); - - test('request with multipart file', () async { - when( - client.send(any), - ).thenAnswer( - (_) => Future.value( - http.StreamedResponse( - Stream.fromIterable( - [utf8.encode('{"data":{}}')], - ), - 200, - ), - ), - ); - - final query = Operation( - documentNode: parseString('{}'), - variables: { - 'files': [ - http.MultipartFile.fromString( - 'field-1', - 'just plain text 1', - filename: 'sample_upload1.txt', - contentType: MediaType('text', 'plain'), - ), - http.MultipartFile.fromString( - 'field-2', - 'just plain text 2', - filename: 'sample_upload2.txt', - contentType: MediaType('text', 'plain'), - ), - ], - }, - ); - - await execute( - link: link, - operation: query, - ).first; - - final http.MultipartRequest captured = verify( - client.send(captureAny), - ).captured.single; - - final req = await captured.finalize().bytesToString(); - - expect( - req - .replaceAll( - RegExp('--dart-http-boundary-.{51}'), - '--dart-http-boundary-REPLACED', - ) - .replaceAll( - '\r\n', - '\n', - ), - r'''--dart-http-boundary-REPLACED -content-disposition: form-data; name="operations" - -{"operationName":null,"variables":{"files":[null,null]},"query":"query {\n \n}"} ---dart-http-boundary-REPLACED -content-disposition: form-data; name="map" - -{"0":["variables.files.0"],"1":["variables.files.1"]} ---dart-http-boundary-REPLACED -content-type: text/plain; charset=utf-8 -content-disposition: form-data; name="0"; filename="sample_upload1.txt" - -just plain text 1 ---dart-http-boundary-REPLACED -content-type: text/plain; charset=utf-8 -content-disposition: form-data; name="1"; filename="sample_upload2.txt" - -just plain text 2 ---dart-http-boundary-REPLACED-- -''', - ); - }); - }); -} diff --git a/packages/graphql/test/link/link_test.dart b/packages/graphql/test/link/link_test.dart deleted file mode 100644 index a5373c462..000000000 --- a/packages/graphql/test/link/link_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:graphql/client.dart'; -import 'package:graphql/src/link/operation.dart'; -import 'package:test/test.dart'; - -void main() { - group('link', () { - test('multiple', () async { - final link1 = Link( - request: (Operation op, [NextLink forward]) { - return null; - }, - ); - - final link2 = Link( - request: (Operation op, [NextLink forward]) { - return null; - }, - ); - - final link3 = Link( - request: (Operation op, [NextLink forward]) { - return null; - }, - ); - - final linksFrom = Link.from([link1, link2, link3]); - - final linksConcat = link1..concat(link2)..concat(link3); - - var resultConcat = await execute(link: linksConcat); - var resultFrom = await execute(link: linksFrom); - - expect(resultConcat, resultFrom); - }); - }); -} diff --git a/packages/graphql/test/multipart_upload_io_test.dart b/packages/graphql/test/multipart_upload_io_test.dart deleted file mode 100644 index de2e56d79..000000000 --- a/packages/graphql/test/multipart_upload_io_test.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:gql/language.dart'; -@TestOn("vm") -import 'package:test/test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:http/http.dart' as http; -import 'dart:io' as io; - -import 'package:graphql/client.dart'; - -import 'helpers.dart'; - -class MockHttpClient extends Mock implements http.Client {} - -void main() { - HttpLink httpLink; - AuthLink authLink; - Link link; - GraphQLClient graphQLClientClient; - MockHttpClient mockHttpClient; - - group( - 'upload', - () { - const String uploadMutation = r''' - mutation($files: [Upload!]!) { - multipleUpload(files: $files) { - id - filename - mimetype - path - } - } - '''; - - setUp(() { - mockHttpClient = MockHttpClient(); - - when(mockHttpClient.send(any)).thenAnswer((Invocation a) async { - return simpleResponse(body: '{"data": {}}'); - }); - - httpLink = HttpLink( - uri: 'http://localhost:3001/graphql', httpClient: mockHttpClient); - - authLink = AuthLink( - getToken: () async => 'Bearer my-special-bearer-token', - ); - - link = authLink.concat(httpLink); - - graphQLClientClient = GraphQLClient( - cache: getTestCache(), - link: link, - ); - }); - - test( - 'upload with io.File instance deprecation warning', - overridePrint((log) async { - final MutationOptions _options = MutationOptions( - documentNode: parseString(uploadMutation), - variables: { - 'files': [ - io.File('pubspec.yaml'), - ], - }, - ); - final QueryResult r = await graphQLClientClient.mutate(_options); - - expect(r.exception, isNull); - expect(r.data, isNotNull); - expect(log, hasLength(5)); - final warningMessage = r''' -⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️ DEPRECATION WARNING ⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️ - -Please do not use `File` direcly anymore. Instead, use -`MultipartFile`. There's also a utitlity method to help you -`import 'package:graphql/utilities.dart' show multipartFileFrom;` - -⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️ DEPRECATION WARNING ⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️ - '''; - expect(log[0], warningMessage); - expect(log[1], warningMessage); - expect(log[2], warningMessage); - expect(log[3], warningMessage); - expect(log[4], warningMessage); - }), - ); - }, - onPlatform: { - "!vm": Skip("This test is only for VM"), - }, - ); -} diff --git a/packages/graphql/test/multipart_upload_test.dart b/packages/graphql/test/multipart_upload_test.dart deleted file mode 100644 index 9212a5e84..000000000 --- a/packages/graphql/test/multipart_upload_test.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'package:gql/language.dart'; -import 'package:http_parser/http_parser.dart'; -import 'package:test/test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:http/http.dart' as http; - -import 'package:graphql/client.dart'; - -import './helpers.dart'; - -class MockHttpClient extends Mock implements http.Client {} - -NormalizedInMemoryCache getTestCache() => NormalizedInMemoryCache( - dataIdFromObject: typenameDataIdFromObject, - ); - -void main() { - HttpLink httpLink; - AuthLink authLink; - Link link; - GraphQLClient graphQLClientClient; - MockHttpClient mockHttpClient; - - group('upload', () { - const String uploadMutation = r''' - mutation($files: [Upload!]!) { - multipleUpload(files: $files) { - id - filename - mimetype - path - } - } - '''; - - setUp(() { - mockHttpClient = MockHttpClient(); - - httpLink = HttpLink( - uri: 'http://localhost:3001/graphql', httpClient: mockHttpClient); - - authLink = AuthLink( - getToken: () async => 'Bearer my-special-bearer-token', - ); - - link = authLink.concat(httpLink); - - graphQLClientClient = GraphQLClient( - cache: getTestCache(), - link: link, - ); - }); - - test('upload success', () async { - Future expectUploadBody( - http.ByteStream bodyBytesStream, String boundary) async { - final List expectContinuationList = (() { - int i = 0; - return [ - // ExpectString - (List actual, String expected) => expect( - String.fromCharCodes(actual.sublist(i, i += expected.length)), - expected), - // ExpectBytes - (List actual, List expected) => - expect(actual.sublist(i, i += expected.length), expected), - // Expect final length - (int expectedLength) => expect(i, expectedLength), - ]; - })(); - final Function expectContinuationString = expectContinuationList[0]; - final Function expectContinuationBytes = expectContinuationList[1]; - final Function expectContinuationLength = expectContinuationList[2]; - final bodyBytes = await bodyBytesStream.toBytes(); - expectContinuationString(bodyBytes, '--'); - expectContinuationString(bodyBytes, boundary); - expectContinuationString(bodyBytes, - '\r\ncontent-disposition: form-data; name="operations"\r\n\r\n'); - // operationName of unamed operations is "UNNAMED/" + document.hashCode.toString() - expectContinuationString(bodyBytes, - r'{"operationName":null,"variables":{"files":[null,null]},"query":"mutation($files: [Upload!]!) {\n multipleUpload(files: $files) {\n id\n filename\n mimetype\n path\n }\n}"}'); - expectContinuationString(bodyBytes, '\r\n--'); - expectContinuationString(bodyBytes, boundary); - expectContinuationString(bodyBytes, - '\r\ncontent-disposition: form-data; name="map"\r\n\r\n{"0":["variables.files.0"],"1":["variables.files.1"]}'); - expectContinuationString(bodyBytes, '\r\n--'); - expectContinuationString(bodyBytes, boundary); - expectContinuationString(bodyBytes, - '\r\ncontent-type: image/jpeg\r\ncontent-disposition: form-data; name="0"; filename="sample_upload.jpg"\r\n\r\n'); - expectContinuationBytes(bodyBytes, [0, 1, 254, 255]); - expectContinuationString(bodyBytes, '\r\n--'); - expectContinuationString(bodyBytes, boundary); - expectContinuationString(bodyBytes, - '\r\ncontent-type: text/plain; charset=utf-8\r\ncontent-disposition: form-data; name="1"; filename="sample_upload.txt"\r\n\r\n'); - expectContinuationString(bodyBytes, 'just plain text'); - expectContinuationString(bodyBytes, '\r\n--'); - expectContinuationString(bodyBytes, boundary); - expectContinuationString(bodyBytes, '--\r\n'); - expectContinuationLength(bodyBytes.lengthInBytes); - } - - http.ByteStream bodyBytes; - when(mockHttpClient.send(any)).thenAnswer((Invocation a) async { - bodyBytes = (a.positionalArguments[0] as http.BaseRequest).finalize(); - return simpleResponse(body: r''' -{ - "data": { - "multipleUpload": [ - { - "id": "r1odc4PAz", - "filename": "sample_upload.jpg", - "mimetype": "image/jpeg", - "path": "./uploads/r1odc4PAz-sample_upload.jpg" - }, - { - "id": "5Ea18qlMur", - "filename": "sample_upload.txt", - "mimetype": "text/plain", - "path": "./uploads/5Ea18qlMur-sample_upload.txt" - } - ] - } -} - '''); - }); - - final MutationOptions _options = MutationOptions( - documentNode: parseString(uploadMutation), - variables: { - 'files': [ - http.MultipartFile.fromBytes( - '', - [0, 1, 254, 255], - filename: 'sample_upload.jpg', - contentType: MediaType('image', 'jpeg'), - ), - http.MultipartFile.fromString( - '', - 'just plain text', - filename: 'sample_upload.txt', - contentType: MediaType('text', 'plain'), - ), - ], - }, - ); - final QueryResult r = await graphQLClientClient.mutate(_options); - - expect(r.exception, isNull); - expect(r.data, isNotNull); - - final http.MultipartRequest request = - verify(mockHttpClient.send(captureAny)).captured.first - as http.MultipartRequest; - expect(request.method, 'post'); - expect(request.url.toString(), 'http://localhost:3001/graphql'); - expect(request.headers['accept'], '*/*'); - expect( - request.headers['Authorization'], 'Bearer my-special-bearer-token'); - final List contentTypeStringSplit = - request.headers['content-type'].split('; boundary='); - expect(contentTypeStringSplit[0], 'multipart/form-data'); - await expectUploadBody(bodyBytes, contentTypeStringSplit[1]); - - final List> multipleUpload = - (r.data['multipleUpload'] as List) - .cast>(); - - expect(multipleUpload, >[ - { - 'id': 'r1odc4PAz', - 'filename': 'sample_upload.jpg', - 'mimetype': 'image/jpeg', - 'path': './uploads/r1odc4PAz-sample_upload.jpg' - }, - { - 'id': '5Ea18qlMur', - 'filename': 'sample_upload.txt', - 'mimetype': 'text/plain', - 'path': './uploads/5Ea18qlMur-sample_upload.txt' - }, - ]); - }); - - //test('upload fail error response', () { - // const String responseBody = json.encode({ - // "errors":[ - // { - // "message": r'Variable "$files" of required type "[Upload!]!" was not provided.', - // "locations": [{ "line" :1, "column" :14 }], - // "extensions": { - // "code": "INTERNAL_SERVER_ERROR", - // "exception": { - // "stacktrace": [ r'GraphQLError: Variable "$files" of required type "[Upload!]!" was not provided.', ... ] - // } - // } - // } - // ] - // }); - // const int statusCode = 400; - //}); - }); -} diff --git a/packages/graphql/test/socket_client_test.dart b/packages/graphql/test/socket_client_test.dart deleted file mode 100644 index 1baa7cd76..000000000 --- a/packages/graphql/test/socket_client_test.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:gql/language.dart'; -import 'package:graphql/client.dart'; -import 'package:graphql/src/link/operation.dart'; -import 'package:graphql/src/socket_client.dart' - show SocketClient, SocketConnectionState; -import 'package:graphql/src/websocket/messages.dart'; -import 'package:test/test.dart'; - -import 'helpers.dart'; - -void main() { - group('SocketClient without payload', () { - SocketClient socketClient; - setUp(overridePrint((log) { - socketClient = SocketClient( - 'ws://echo.websocket.org', - protocols: null, - randomBytesForUuid: Uint8List.fromList( - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), - ); - })); - tearDown(overridePrint((log) async { - await socketClient.dispose(); - })); - test('connection', () async { - await expectLater( - socketClient.connectionState.asBroadcastStream(), - emitsInOrder( - [ - SocketConnectionState.CONNECTING, - SocketConnectionState.CONNECTED, - ], - ), - ); - }); - test('subscription data', () async { - final payload = SubscriptionRequest( - Operation(documentNode: parseString('subscription {}')), - ); - final waitForConnection = true; - final subscriptionDataStream = - socketClient.subscribe(payload, waitForConnection); - await socketClient.connectionState - .where((state) => state == SocketConnectionState.CONNECTED) - .first; - - // ignore: unawaited_futures - socketClient.socket.stream - .where((message) => - message == - r'{"type":"start","id":"01020304-0506-4708-890a-0b0c0d0e0f10","payload":{"operationName":null,"query":"subscription {\n \n}","variables":{}}}') - .first - .then((_) { - socketClient.socket.add(jsonEncode({ - 'type': 'data', - 'id': '01020304-0506-4708-890a-0b0c0d0e0f10', - 'payload': { - 'data': {'foo': 'bar'}, - 'errors': ['error and data can coexist'] - } - })); - }); - - await expectLater( - subscriptionDataStream, - emits( - SubscriptionData( - '01020304-0506-4708-890a-0b0c0d0e0f10', - {'foo': 'bar'}, - ['error and data can coexist'], - ), - ), - ); - }); - test('resubscribe', () async { - final payload = SubscriptionRequest( - Operation(documentNode: gql('subscription {}')), - ); - final waitForConnection = true; - final subscriptionDataStream = - socketClient.subscribe(payload, waitForConnection); - - socketClient.onConnectionLost(); - - await socketClient.connectionState - .where((state) => state == SocketConnectionState.CONNECTED) - .first; - - // ignore: unawaited_futures - socketClient.socket.stream - .where((message) => - message == - r'{"type":"start","id":"01020304-0506-4708-890a-0b0c0d0e0f10","payload":{"operationName":null,"query":"subscription {\n \n}","variables":{}}}') - .first - .then((_) { - socketClient.socket.add(jsonEncode({ - 'type': 'data', - 'id': '01020304-0506-4708-890a-0b0c0d0e0f10', - 'payload': { - 'data': {'foo': 'bar'}, - 'errors': ['error and data can coexist'] - } - })); - }); - - await expectLater( - subscriptionDataStream, - emits( - SubscriptionData( - '01020304-0506-4708-890a-0b0c0d0e0f10', - {'foo': 'bar'}, - ['error and data can coexist'], - ), - ), - ); - }); - }, tags: "integration"); - - group('SocketClient with const payload', () { - SocketClient socketClient; - const initPayload = {'token': 'mytoken'}; - - setUp(overridePrint((log) { - socketClient = SocketClient( - 'ws://echo.websocket.org', - config: SocketClientConfig(initPayload: () => initPayload), - ); - })); - - tearDown(overridePrint((log) async { - await socketClient.dispose(); - })); - - test('connection', () async { - await socketClient.connectionState - .where((state) => state == SocketConnectionState.CONNECTED) - .first; - - await expectLater(socketClient.socket.stream.map((s) { - return jsonDecode(s)['payload']; - }), emits(initPayload)); - }); - }); - - group('SocketClient with future payload', () { - SocketClient socketClient; - const initPayload = {'token': 'mytoken'}; - - setUp(overridePrint((log) { - socketClient = SocketClient( - 'ws://echo.websocket.org', - config: SocketClientConfig( - initPayload: () async { - await Future.delayed(Duration(seconds: 3)); - return initPayload; - }, - ), - ); - })); - - tearDown(overridePrint((log) async { - await socketClient.dispose(); - })); - - test('connection', () async { - await socketClient.connectionState - .where((state) => state == SocketConnectionState.CONNECTED) - .first; - - await expectLater(socketClient.socket.stream.map((s) { - return jsonDecode(s)['payload']; - }), emits(initPayload)); - }); - }); -} diff --git a/packages/graphql/test/websocket_legacy_io_test.dart b/packages/graphql/test/websocket_legacy_io_test.dart deleted file mode 100644 index c44317bcc..000000000 --- a/packages/graphql/test/websocket_legacy_io_test.dart +++ /dev/null @@ -1,96 +0,0 @@ -@TestOn('vm') - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:gql/language.dart'; -import 'package:test/test.dart'; - -import 'package:graphql/src/link/operation.dart'; -import 'package:graphql/src/websocket/messages.dart'; - -import 'package:graphql/legacy_socket_api/legacy_socket_client.dart'; - -import 'helpers.dart'; - -void main() { - group( - 'SocketClient', - () { - // ignore: deprecated_member_use_from_same_package - SocketClient socketClient; - setUp(overridePrint((log) { - // ignore: deprecated_member_use_from_same_package - socketClient = SocketClient( - 'ws://echo.websocket.org', - protocols: null, - randomBytesForUuid: Uint8List.fromList( - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), - ); - })); - tearDown(overridePrint((log) async { - await socketClient.dispose(); - })); - test('connection', () async { - await expectLater( - socketClient.connectionState.asBroadcastStream(), - emitsInOrder( - [ - SocketConnectionState.CONNECTING, - SocketConnectionState.CONNECTED, - ], - ), - ); - }); - test('subscription data', () async { - final payload = SubscriptionRequest( - Operation(documentNode: parseString('subscription {}')), - ); - final waitForConnection = true; - final subscriptionDataStream = - socketClient.subscribe(payload, waitForConnection); - await socketClient.connectionState - .where((state) => state == SocketConnectionState.CONNECTED) - .first; - - // ignore: unawaited_futures - socketClient.stream - .where( - (message) => - message == - r'{"type":"start","id":"01020304-0506-4708-890a-0b0c0d0e0f10","payload":{"operationName":null,"query":"subscription {\n \n}","variables":{}}}', - ) - .first - .then( - (_) { - socketClient.socket.add( - jsonEncode({ - 'type': 'data', - 'id': '01020304-0506-4708-890a-0b0c0d0e0f10', - 'payload': { - 'data': {'foo': 'bar'}, - 'errors': ['error and data can coexist'] - } - }), - ); - }, - ); - - await expectLater( - subscriptionDataStream, - emits( - SubscriptionData( - '01020304-0506-4708-890a-0b0c0d0e0f10', - {'foo': 'bar'}, - ['error and data can coexist'], - ), - ), - ); - }); - }, - tags: "integration", - onPlatform: { - "!vm": Skip("This test is only for VM"), - }, - ); -} diff --git a/packages/graphql/test/websocket_legacy_test.dart b/packages/graphql/test/websocket_legacy_test.dart deleted file mode 100644 index 042c0c190..000000000 --- a/packages/graphql/test/websocket_legacy_test.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:test/test.dart'; - -import 'package:graphql/legacy_socket_api/legacy_socket_link.dart'; -import 'package:graphql/legacy_socket_api/legacy_socket_client.dart'; - -import 'helpers.dart'; - -void main() { - group('Link Websocket', () { - test('simple connection', overridePrint((List log) { - // ignore: deprecated_member_use_from_same_package - WebSocketLink( - url: 'ws://echo.websocket.org', - // ignore: deprecated_member_use_from_same_package - headers: {'foo': 'bar'}, - ); - expect(log, [ - 'WARNING: Using direct websocket headers which will be removed soon, ' - 'as it is incompatable with dart:html. ' - 'If you need this direct header access, ' - 'please comment on this PR with details on your usecase: ' - 'https://github.com/zino-app/graphql-flutter/pull/323' - ]); - })); - }); - - group('LegacyInitOperation', () { - test('null payload', () { - // ignore: deprecated_member_use_from_same_package - final operation = LegacyInitOperation(null); - expect(operation.toJson(), {'type': 'connection_init'}); - }); - test('simple payload', () { - // ignore: deprecated_member_use_from_same_package - final operation = LegacyInitOperation(42); - expect(operation.toJson(), {'type': 'connection_init', 'payload': '42'}); - }); - test('complex payload', () { - // ignore: deprecated_member_use_from_same_package - final operation = LegacyInitOperation({ - 'value': 42, - 'nested': { - 'number': [3, 7], - 'string': ['foo', 'bar'] - } - }); - expect(operation.toJson(), { - 'type': 'connection_init', - 'payload': - '{"value":42,"nested":{"number":[3,7],"string":["foo","bar"]}}' - }); - }); - }); -} diff --git a/packages/graphql/test/websocket_test.dart b/packages/graphql/test/websocket_test.dart deleted file mode 100644 index c70077808..000000000 --- a/packages/graphql/test/websocket_test.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:test/test.dart'; -import 'package:graphql/src/websocket/messages.dart' show InitOperation; - -void main() { - group('InitOperation', () { - test('null payload', () { - // ignore: deprecated_member_use_from_same_package - final operation = InitOperation(null); - expect(operation.toJson(), {'type': 'connection_init'}); - }); - test('simple payload', () { - // ignore: deprecated_member_use_from_same_package - final operation = InitOperation(42); - expect(operation.toJson(), {'type': 'connection_init', 'payload': 42}); - }); - test('complex payload', () { - // ignore: deprecated_member_use_from_same_package - final operation = InitOperation({ - 'value': 42, - 'nested': { - 'number': [3, 7], - 'string': ['foo', 'bar'] - } - }); - expect(operation.toJson(), { - 'type': 'connection_init', - 'payload': { - 'value': 42, - 'nested': { - 'number': [3, 7], - 'string': ['foo', 'bar'] - } - } - }); - }); - }); -} diff --git a/packages/graphql_flutter/lib/src/widgets/subscription.dart b/packages/graphql_flutter/lib/src/widgets/subscription.dart index 30a89e3bc..3030725d1 100644 --- a/packages/graphql_flutter/lib/src/widgets/subscription.dart +++ b/packages/graphql_flutter/lib/src/widgets/subscription.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:connectivity/connectivity.dart'; import 'package:flutter/widgets.dart'; import 'package:gql/language.dart'; +import 'package:gql_exec/gql_exec.dart'; import 'package:graphql/client.dart'; import 'package:graphql/internal.dart'; import 'package:graphql_flutter/src/widgets/graphql_provider.dart'; @@ -42,7 +43,7 @@ class _SubscriptionState extends State> { bool _loading = true; T _data; dynamic _error; - StreamSubscription _subscription; + StreamSubscription _subscription; ConnectivityResult _currentConnectivityResult; StreamSubscription _networkSubscription; @@ -50,13 +51,15 @@ class _SubscriptionState extends State> { void _initSubscription() { final GraphQLClient client = GraphQLProvider.of(context).value; assert(client != null); - final Operation operation = Operation( - documentNode: parseString(widget.query), + final Request request = Request( + operation: Operation( + document: parseString(widget.query), + operationName: widget.operationName, + ), variables: widget.variables, - operationName: widget.operationName, ); - final Stream stream = client.subscribe(operation); + final Stream stream = client.subscribe(request); if (_subscription == null) { // Set the initial value for the first time. @@ -109,7 +112,7 @@ class _SubscriptionState extends State> { super.dispose(); } - void _onData(final FetchResult message) { + void _onData(final Response message) { setState(() { _loading = false; _data = message.data as T; @@ -121,7 +124,7 @@ class _SubscriptionState extends State> { setState(() { _loading = false; _data = null; - _error = (error is SubscriptionError) ? error.payload : error; + _error = error; }); } diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index fc97d6062..aa20c4586 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -9,12 +9,11 @@ authors: homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: graphql: ^3.0.1-beta.2 + gql_exec: ^0.2.2 flutter: sdk: flutter meta: ^1.1.6 - path: ^1.6.2 path_provider: ^1.1.0 - rxdart: ^0.23.1 connectivity: ^0.4.4 dev_dependencies: pedantic: ^1.8.0+1 From 7499323673af6ea6c9889c828fc8ff80042f1a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kl=C4=81vs=20Pried=C4=ABtis?= Date: Sun, 16 Feb 2020 21:50:40 +0200 Subject: [PATCH 002/118] feat: move to DocumentNode-only documents BREAKING CHANGE: the deprecated string documents are no longer supported --- packages/graphql/README.md | 12 ++-- packages/graphql/example/bin/main.dart | 6 +- .../lib/src/core/observable_query.dart | 2 +- .../graphql/lib/src/core/query_manager.dart | 2 +- .../graphql/lib/src/core/query_options.dart | 67 +++---------------- .../lib/src/core/raw_operation_data.dart | 42 ++---------- .../test/anonymous_operations_test.dart | 4 +- .../test/core/raw_operation_data_test.dart | 26 +++---- .../graphql/test/graphql_client_test.dart | 8 +-- .../lib/src/widgets/mutation.dart | 2 - .../lib/src/widgets/query.dart | 2 - 11 files changed, 47 insertions(+), 126 deletions(-) diff --git a/packages/graphql/README.md b/packages/graphql/README.md index be46131c6..b9a5f3d7d 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -110,7 +110,7 @@ const String readRepositories = r''' Then create a `QueryOptions` object: -> **NB:** for `documentNode` - Use our built-in help function - `gql(query)` to convert your document string to **ASTs** `documentNode`. +> **NB:** for `document` - Use our built-in help function - `gql(query)` to convert your document string to **ASTs** `document`. In our case, we need to pass `nRepositories` variable and the document name is `readRepositories`. @@ -119,7 +119,7 @@ In our case, we need to pass `nRepositories` variable and the document name is ` const int nRepositories = 50; final QueryOptions options = QueryOptions( - documentNode: gql(readRepositories), + document: gql(readRepositories), variables: { 'nRepositories': nRepositories, }, @@ -166,7 +166,7 @@ Then instead of the `QueryOptions`, for mutations we will `MutationOptions`, whi // ... final MutationOptions options = MutationOptions( - documentNode: gql(addStar), + document: gql(addStar), variables: { 'starrableId': repositoryID, }, @@ -201,7 +201,7 @@ if (isStarred) { ### AST documents > We are deprecating `document` and recommend you update your application to use -`documentNode` instead. `document` will be removed from the api in a future version. +`document` instead. `document` will be removed from the api in a future version. For example: @@ -209,7 +209,7 @@ For example: // ... final MutationOptions options = MutationOptions( - documentNode: gql(addStar), + document: gql(addStar), variables: { 'starrableId': repositoryID, }, @@ -238,7 +238,7 @@ import 'package:gql/add_star.ast.g.dart' as add_star; // ... final MutationOptions options = MutationOptions( - documentNode: add_star.document, + document: add_star.document, variables: { 'starrableId': repositoryID, }, diff --git a/packages/graphql/example/bin/main.dart b/packages/graphql/example/bin/main.dart index ab184ad68..4e994cf35 100644 --- a/packages/graphql/example/bin/main.dart +++ b/packages/graphql/example/bin/main.dart @@ -38,7 +38,7 @@ void query() async { const int nRepositories = 50; final QueryOptions options = QueryOptions( - documentNode: gql(readRepositories), + document: gql(readRepositories), variables: { 'nRepositories': nRepositories, }, @@ -70,7 +70,7 @@ void starRepository(String repositoryID) async { final GraphQLClient _client = client(); final MutationOptions options = MutationOptions( - documentNode: gql(addStar), + document: gql(addStar), variables: { 'starrableId': repositoryID, }, @@ -103,7 +103,7 @@ void removeStarFromRepository(String repositoryID) async { final GraphQLClient _client = client(); final MutationOptions options = MutationOptions( - documentNode: gql(removeStar), + document: gql(removeStar), variables: { 'starrableId': repositoryID, }, diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index b964b1cfc..54d7ebc8a 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -145,7 +145,7 @@ class ObservableQuery { final combinedOptions = QueryOptions( fetchPolicy: FetchPolicy.noCache, errorPolicy: options.errorPolicy, - documentNode: fetchMoreOptions.documentNode ?? options.documentNode, + document: fetchMoreOptions.document ?? options.document, context: options.context, variables: { ...options.variables, diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index fdad03aa7..da7e282da 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -106,7 +106,7 @@ class QueryManager { // create a new request to execute final Request request = Request( operation: Operation( - document: options.documentNode, + document: options.document, operationName: options.operationName, ), variables: options.variables, diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index 6605b92a9..21a0c1f1b 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -70,6 +70,7 @@ class Policies { overrides?.fetch ?? fetch, overrides?.error ?? error, ); + operator ==(Object other) => other is Policies && fetch == other.fetch && error == other.error; } @@ -77,17 +78,13 @@ class Policies { /// Base options. class BaseOptions extends RawOperationData { BaseOptions({ - @Deprecated('The "document" option has been deprecated, use "documentNode" instead') - String document, - DocumentNode documentNode, + @required DocumentNode document, Map variables, this.policies, this.context, this.optimisticResult, }) : super( - // ignore: deprecated_member_use_from_same_package document: document, - documentNode: documentNode, variables: variables, ); @@ -108,9 +105,7 @@ class BaseOptions extends RawOperationData { /// Query options. class QueryOptions extends BaseOptions { QueryOptions({ - @Deprecated('The "document" option has been deprecated, use "documentNode" instead') - String document, - DocumentNode documentNode, + @required DocumentNode document, Map variables, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, @@ -119,9 +114,7 @@ class QueryOptions extends BaseOptions { Context context, }) : super( policies: Policies(fetch: fetchPolicy, error: errorPolicy), - // ignore: deprecated_member_use_from_same_package document: document, - documentNode: documentNode, variables: variables, context: context, optimisticResult: optimisticResult, @@ -139,9 +132,7 @@ typedef OnError = void Function(OperationException error); /// Mutation options class MutationOptions extends BaseOptions { MutationOptions({ - @Deprecated('The "document" option has been deprecated, use "documentNode" instead') - String document, - DocumentNode documentNode, + @required DocumentNode document, Map variables, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, @@ -151,9 +142,7 @@ class MutationOptions extends BaseOptions { this.onError, }) : super( policies: Policies(fetch: fetchPolicy, error: errorPolicy), - // ignore: deprecated_member_use_from_same_package document: document, - documentNode: documentNode, variables: variables, context: context, ); @@ -253,9 +242,7 @@ class MutationCallbacks { // ObservableQuery options class WatchQueryOptions extends QueryOptions { WatchQueryOptions({ - @Deprecated('The "document" option has been deprecated, use "documentNode" instead') - String document, - DocumentNode documentNode, + @required DocumentNode document, Map variables, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, @@ -265,9 +252,7 @@ class WatchQueryOptions extends QueryOptions { this.eagerlyFetchResults, Context context, }) : super( - // ignore: deprecated_member_use_from_same_package document: document, - documentNode: documentNode, variables: variables, fetchPolicy: fetchPolicy, errorPolicy: errorPolicy, @@ -292,7 +277,7 @@ class WatchQueryOptions extends QueryOptions { WatchQueryOptions a, WatchQueryOptions b, ) { - if (a.documentNode != b.documentNode) { + if (a.document != b.document) { return true; } @@ -313,7 +298,7 @@ class WatchQueryOptions extends QueryOptions { } WatchQueryOptions copy() => WatchQueryOptions( - documentNode: documentNode, + document: document, variables: variables, fetchPolicy: fetchPolicy, errorPolicy: errorPolicy, @@ -334,33 +319,12 @@ typedef dynamic UpdateQuery( /// options for fetchmore operations class FetchMoreOptions { FetchMoreOptions({ - @Deprecated('The "document" option has been deprecated, use "documentNode" instead') - String document, - DocumentNode documentNode, + @required this.document, this.variables = const {}, @required this.updateQuery, - }) : assert( - // ignore: deprecated_member_use_from_same_package - _mutuallyExclusive(document, documentNode), - '"document" or "documentNode" options are mutually exclusive.', - ), - assert(updateQuery != null), - this.documentNode = - // ignore: deprecated_member_use_from_same_package - documentNode ?? document != null ? parseString(document) : null; - - DocumentNode documentNode; - - /// A string representation of [documentNode] - @Deprecated( - 'The "document" option has been deprecated, use "documentNode" instead') - String get document => printNode(documentNode); - - @Deprecated( - 'The "document" option has been deprecated, use "documentNode" instead') - set document(value) { - documentNode = parseString(value); - } + }) : assert(updateQuery != null); + + DocumentNode document; final Map variables; @@ -368,12 +332,3 @@ class FetchMoreOptions { /// with the result data already in the cache UpdateQuery updateQuery; } - -bool _mutuallyExclusive( - Object a, - Object b, { - bool required = false, -}) => - (!required && a == null && b == null) || - (a != null && b == null) || - (a == null && b != null); diff --git a/packages/graphql/lib/src/core/raw_operation_data.dart b/packages/graphql/lib/src/core/raw_operation_data.dart index 5d003a9e8..415e0a756 100644 --- a/packages/graphql/lib/src/core/raw_operation_data.dart +++ b/packages/graphql/lib/src/core/raw_operation_data.dart @@ -2,50 +2,21 @@ import 'dart:collection' show SplayTreeMap; import 'dart:convert' show json; import 'package:gql/ast.dart'; -import 'package:gql/language.dart'; import 'package:graphql/src/utilities/get_from_ast.dart'; +import 'package:meta/meta.dart'; class RawOperationData { RawOperationData({ - @Deprecated('The "document" option has been deprecated, use "documentNode" instead') - String document, - DocumentNode documentNode, + @required this.document, Map variables, String operationName, - }) : assert( - // ignore: deprecated_member_use_from_same_package - document != null || documentNode != null, - 'Either a "document" or "documentNode" option is required. ' - 'You must specify your GraphQL document in the query options.', - ), - // todo: Investigate why this assertion is failing - // assert( - // (document != null && documentNode == null) || - // (document == null && documentNode != null), - // '"document" or "documentNode" options are mutually exclusive.', - // ), - // ignore: deprecated_member_use_from_same_package - documentNode = documentNode ?? parseString(document), - _operationName = operationName, + }) : _operationName = operationName, variables = SplayTreeMap.of( variables ?? const {}, ); /// A GraphQL document that consists of a single query to be sent down to the server. - DocumentNode documentNode; - - /// A string representation of [documentNode] - @Deprecated( - 'The "document" option has been deprecated, use "documentNode" instead', - ) - String get document => printNode(documentNode); - - @Deprecated( - 'The "document" option has been deprecated, use "documentNode" instead', - ) - set document(value) { - documentNode = parseString(value); - } + DocumentNode document; /// A map going from variable name to variable value, where the variables are used /// within the GraphQL query. @@ -55,7 +26,7 @@ class RawOperationData { /// The last operation name appearing in the contained document. String get operationName { - _operationName ??= getLastOperationName(documentNode); + _operationName ??= getLastOperationName(document); return _operationName; } @@ -65,7 +36,7 @@ class RawOperationData { // TODO remove $document from key? A bit redundant, though that's not the worst thing String get _identifier { _documentIdentifier ??= - operationName ?? 'UNNAMED/' + documentNode.hashCode.toString(); + operationName ?? 'UNNAMED/' + document.hashCode.toString(); return _documentIdentifier; } @@ -81,7 +52,6 @@ class RawOperationData { ); // TODO: document is being depracated, find ways for generating key - // ignore: deprecated_member_use_from_same_package return '$document|$encodedVariables|$_identifier'; } } diff --git a/packages/graphql/test/anonymous_operations_test.dart b/packages/graphql/test/anonymous_operations_test.dart index 3dbcdca6f..e69536cbe 100644 --- a/packages/graphql/test/anonymous_operations_test.dart +++ b/packages/graphql/test/anonymous_operations_test.dart @@ -50,7 +50,7 @@ void main() { group('query', () { test('successful query', () async { final WatchQueryOptions _options = WatchQueryOptions( - documentNode: parseString(readRepositories), + document: parseString(readRepositories), variables: {}, ); @@ -129,7 +129,7 @@ void main() { group('mutation', () { test('successful mutation', () async { final MutationOptions _options = MutationOptions( - documentNode: parseString(addStar), + document: parseString(addStar), ); when( diff --git a/packages/graphql/test/core/raw_operation_data_test.dart b/packages/graphql/test/core/raw_operation_data_test.dart index 05c189ca2..60fe97622 100644 --- a/packages/graphql/test/core/raw_operation_data_test.dart +++ b/packages/graphql/test/core/raw_operation_data_test.dart @@ -7,7 +7,7 @@ void main() { group('single operation', () { test('query without name', () { final opData = RawOperationData( - documentNode: parseString('query {}'), + document: parseString('query {}'), ); expect(opData.operationName, null); @@ -15,7 +15,7 @@ void main() { test('query with explicit name', () { final opData = RawOperationData( - documentNode: parseString('query Operation {}'), + document: parseString('query Operation {}'), operationName: 'Operation', ); @@ -24,7 +24,7 @@ void main() { test('mutation with explicit name', () { final opData = RawOperationData( - documentNode: parseString('mutation Operation {}'), + document: parseString('mutation Operation {}'), operationName: 'Operation', ); @@ -33,7 +33,7 @@ void main() { test('subscription with explicit name', () { final opData = RawOperationData( - documentNode: parseString('subscription Operation {}'), + document: parseString('subscription Operation {}'), operationName: 'Operation', ); @@ -42,7 +42,7 @@ void main() { test('query with implicit name', () { final opData = RawOperationData( - documentNode: parseString('query Operation {}'), + document: parseString('query Operation {}'), ); expect(opData.operationName, 'Operation'); @@ -50,7 +50,7 @@ void main() { test('mutation with implicit name', () { final opData = RawOperationData( - documentNode: parseString('mutation Operation {}'), + document: parseString('mutation Operation {}'), ); expect(opData.operationName, 'Operation'); @@ -58,7 +58,7 @@ void main() { test('subscription with implicit name', () { final opData = RawOperationData( - documentNode: parseString('subscription Operation {}'), + document: parseString('subscription Operation {}'), ); expect(opData.operationName, 'Operation'); @@ -74,7 +74,7 @@ void main() { test('query with explicit name', () { final opData = RawOperationData( - documentNode: parseString(document), + document: parseString(document), operationName: 'OperationQ', ); @@ -83,7 +83,7 @@ void main() { test('mutation with explicit name', () { final opData = RawOperationData( - documentNode: parseString(document), + document: parseString(document), operationName: 'OperationM', ); @@ -92,7 +92,7 @@ void main() { test('subscription with explicit name', () { final opData = RawOperationData( - documentNode: parseString(document), + document: parseString(document), operationName: 'OperationS', ); @@ -101,7 +101,7 @@ void main() { test('query with implicit name', () { final opData = RawOperationData( - documentNode: parseString(document), + document: parseString(document), ); expect(opData.operationName, 'OperationS'); @@ -109,7 +109,7 @@ void main() { test('mutation with implicit name', () { final opData = RawOperationData( - documentNode: parseString(document), + document: parseString(document), ); expect(opData.operationName, 'OperationS'); @@ -117,7 +117,7 @@ void main() { test('subscription with implicit name', () { final opData = RawOperationData( - documentNode: parseString(document), + document: parseString(document), ); expect(opData.operationName, 'OperationS'); diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index 19e983f4e..0d983c75a 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -52,7 +52,7 @@ void main() { group('query', () { test('successful response', () async { final WatchQueryOptions _options = WatchQueryOptions( - documentNode: parseString(readRepositories), + document: parseString(readRepositories), variables: { 'nRepositories': 42, }, @@ -135,7 +135,7 @@ void main() { final QueryResult r = await graphQLClientClient.query( WatchQueryOptions( - documentNode: parseString(readRepositories), + document: parseString(readRepositories), ), ); @@ -158,7 +158,7 @@ void main() { final QueryResult r = await graphQLClientClient.query( WatchQueryOptions( - documentNode: parseString(readRepositories), + document: parseString(readRepositories), ), ); @@ -180,7 +180,7 @@ void main() { group('mutation', () { test('successful mutation', () async { final MutationOptions _options = MutationOptions( - documentNode: parseString(addStar), + document: parseString(addStar), ); when( diff --git a/packages/graphql_flutter/lib/src/widgets/mutation.dart b/packages/graphql_flutter/lib/src/widgets/mutation.dart index 95aed793b..84886a101 100644 --- a/packages/graphql_flutter/lib/src/widgets/mutation.dart +++ b/packages/graphql_flutter/lib/src/widgets/mutation.dart @@ -39,9 +39,7 @@ class MutationState extends State { WatchQueryOptions get _providedOptions { final _options = WatchQueryOptions( - // ignore: deprecated_member_use document: widget.options.document, - documentNode: widget.options.documentNode, variables: widget.options.variables, fetchPolicy: widget.options.fetchPolicy, errorPolicy: widget.options.errorPolicy, diff --git a/packages/graphql_flutter/lib/src/widgets/query.dart b/packages/graphql_flutter/lib/src/widgets/query.dart index d21e4a1ca..66ad85fe9 100644 --- a/packages/graphql_flutter/lib/src/widgets/query.dart +++ b/packages/graphql_flutter/lib/src/widgets/query.dart @@ -39,9 +39,7 @@ class QueryState extends State { final QueryOptions options = widget.options; return WatchQueryOptions( - // ignore: deprecated_member_use document: options.document, - documentNode: options.documentNode, variables: options.variables, fetchPolicy: options.fetchPolicy, errorPolicy: options.errorPolicy, From 4fb205cfd4beab2745c361da18444eda7d7ab9b6 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 9 May 2020 11:21:23 -0500 Subject: [PATCH 003/118] feat: documentNode -> document, dependency issues, reexport links from client (for now), retrieve subscription changes from #533 --- .../lib/extended_bloc/repositories_bloc.dart | 2 +- examples/flutter_bloc/lib/repository.dart | 4 +-- examples/flutter_bloc/pubspec.yaml | 1 - .../starwars/.flutter-plugins-dependencies | 1 - examples/starwars/lib/episode/hero_query.dart | 2 +- .../lib/reviews/review_page_list.dart | 2 +- packages/graphql/example/bin/main.dart | 2 -- packages/graphql/example/pubspec.yaml | 3 --- packages/graphql/lib/client.dart | 3 +++ packages/graphql/pubspec.yaml | 18 +++++++------ packages/graphql_flutter/README.md | 25 +++++++++--------- .../example/.flutter-plugins-dependencies | 2 +- .../example/lib/fetchmore/main.dart | 2 +- .../example/lib/graphql_bloc/bloc.dart | 4 +-- .../example/lib/graphql_widget/main.dart | 4 +-- .../lib/src/widgets/subscription.dart | 3 ++- packages/graphql_flutter/pubspec.yaml | 15 +++++++---- .../test/widgets/query_test.dart | 26 +++++++++---------- 18 files changed, 61 insertions(+), 58 deletions(-) delete mode 100644 examples/starwars/.flutter-plugins-dependencies diff --git a/examples/flutter_bloc/lib/extended_bloc/repositories_bloc.dart b/examples/flutter_bloc/lib/extended_bloc/repositories_bloc.dart index 63fc730b0..3d885b746 100644 --- a/examples/flutter_bloc/lib/extended_bloc/repositories_bloc.dart +++ b/examples/flutter_bloc/lib/extended_bloc/repositories_bloc.dart @@ -10,7 +10,7 @@ class RepositoriesBloc extends GraphqlBloc> { client: client, options: options ?? WatchQueryOptions( - documentNode: parseString(r''' + document: parseString(r''' query ReadRepositories($nRepositories: Int!, $after: String) { viewer { repositories(first: $nRepositories, after: $after) { diff --git a/examples/flutter_bloc/lib/repository.dart b/examples/flutter_bloc/lib/repository.dart index 8cf263e23..bbc41d88f 100644 --- a/examples/flutter_bloc/lib/repository.dart +++ b/examples/flutter_bloc/lib/repository.dart @@ -16,7 +16,7 @@ class GithubRepository { Future getRepositories(int numOfRepositories) async { final WatchQueryOptions _options = WatchQueryOptions( - documentNode: parseString(queries.readRepositories), + document: parseString(queries.readRepositories), variables: { 'nRepositories': numOfRepositories, }, @@ -32,7 +32,7 @@ class GithubRepository { repo.viewerHasStarred ? mutations.removeStar : mutations.addStar; final MutationOptions _options = MutationOptions( - documentNode: parseString(document), + document: parseString(document), variables: { 'starrableId': repo.id, }, diff --git a/examples/flutter_bloc/pubspec.yaml b/examples/flutter_bloc/pubspec.yaml index 289895215..344fe077f 100644 --- a/examples/flutter_bloc/pubspec.yaml +++ b/examples/flutter_bloc/pubspec.yaml @@ -14,7 +14,6 @@ dependencies: path: ../../packages/graphql cupertino_icons: ^0.1.2 flutter_bloc: ^3.2.0 - gql: 0.12.2 equatable: ^0.2.0 dev_dependencies: diff --git a/examples/starwars/.flutter-plugins-dependencies b/examples/starwars/.flutter-plugins-dependencies deleted file mode 100644 index 124de5f63..000000000 --- a/examples/starwars/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity-0.4.8+2/","dependencies":[]},{"name":"path_provider","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.5/","dependencies":[]}],"android":[{"name":"connectivity","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity-0.4.8+2/","dependencies":[]},{"name":"path_provider","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.5/","dependencies":[]}],"macos":[{"name":"connectivity_macos","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_macos-0.1.0+2/","dependencies":[]},{"name":"path_provider_macos","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4/","dependencies":[]}],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"connectivity","dependencies":["connectivity_macos"]},{"name":"connectivity_macos","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos"]},{"name":"path_provider_macos","dependencies":[]}],"date_created":"2020-03-29 14:49:55.731308","version":"1.15.17"} \ No newline at end of file diff --git a/examples/starwars/lib/episode/hero_query.dart b/examples/starwars/lib/episode/hero_query.dart index d96115e2c..47cef03fb 100644 --- a/examples/starwars/lib/episode/hero_query.dart +++ b/examples/starwars/lib/episode/hero_query.dart @@ -12,7 +12,7 @@ class HeroForEpisode extends StatelessWidget { Widget build(BuildContext context) { return Query( options: QueryOptions( - documentNode: gql(r''' + document: gql(r''' query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { __typename diff --git a/examples/starwars/lib/reviews/review_page_list.dart b/examples/starwars/lib/reviews/review_page_list.dart index 7b1f83391..85def029d 100644 --- a/examples/starwars/lib/reviews/review_page_list.dart +++ b/examples/starwars/lib/reviews/review_page_list.dart @@ -13,7 +13,7 @@ class PagingReviews extends StatelessWidget { Widget build(BuildContext context) { return Query( options: QueryOptions( - documentNode: gql(r''' + document: gql(r''' query Reviews($page: Int!) { reviews(page: $page) { page diff --git a/packages/graphql/example/bin/main.dart b/packages/graphql/example/bin/main.dart index 4e994cf35..24ce9ba47 100644 --- a/packages/graphql/example/bin/main.dart +++ b/packages/graphql/example/bin/main.dart @@ -1,8 +1,6 @@ import 'dart:io' show stdout, stderr, exit; import 'package:args/args.dart'; -import 'package:gql_http_link/gql_http_link.dart'; -import 'package:gql_link/gql_link.dart'; import 'package:graphql/client.dart'; import './graphql_operation/mutations/mutations.dart'; diff --git a/packages/graphql/example/pubspec.yaml b/packages/graphql/example/pubspec.yaml index fce16de6b..e8064a72b 100644 --- a/packages/graphql/example/pubspec.yaml +++ b/packages/graphql/example/pubspec.yaml @@ -7,8 +7,5 @@ environment: dependencies: args: - gql_link: ^0.2.3 - gql_http_link: ^0.2.7 graphql: path: .. - diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index 00c91ddaa..d77237fdf 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -10,3 +10,6 @@ export 'package:graphql/src/core/query_options.dart'; export 'package:graphql/src/core/query_result.dart'; export 'package:graphql/src/exceptions/exceptions.dart'; export 'package:graphql/src/graphql_client.dart'; + +export 'package:gql_link/gql_link.dart'; +export 'package:gql_http_link/gql_http_link.dart'; diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index a7a61e490..8bd63e96f 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -1,24 +1,26 @@ name: graphql -description: A stand-alone GraphQL client for Dart, bringing all the features from +description: + A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. version: 3.1.0-beta.4 authors: -- Eus Dima -- Zino Hofmann -- Michael Joseph Rosenthal -- TruongSinh Tran-Nguyen + - Eus Dima + - Zino Hofmann + - Michael Joseph Rosenthal + - TruongSinh Tran-Nguyen homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 path: ^1.6.2 gql: ^0.12.0 gql_exec: ^0.2.2 - gql_link: ^0.2.3 - quiver: '>=2.0.0 <3.0.0' + gql_link: ^0.3.0 + gql_http_link: ^0.2.9 + quiver: ">=2.0.0 <3.0.0" dev_dependencies: pedantic: ^1.8.0+1 mockito: ^4.0.0 test: ^1.5.3 test_coverage: ^0.3.0+1 environment: - sdk: '>=2.6.0 <3.0.0' + sdk: ">=2.6.0 <3.0.0" diff --git a/packages/graphql_flutter/README.md b/packages/graphql_flutter/README.md index ca6bd62ef..72aa21652 100644 --- a/packages/graphql_flutter/README.md +++ b/packages/graphql_flutter/README.md @@ -131,7 +131,6 @@ API key, IAM, and Federated provider authorization could be accomplished through - Making a custom link: [Comment on Issue 173](https://github.com/zino-app/graphql-flutter/issues/173#issuecomment-464435942) - AWS JS SDK `auth-link.ts`: [aws-mobile-appsync-sdk-js:auth-link.ts](https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/master/packages/aws-appsync-auth-link/src/auth-link.ts) - ### Offline Cache The in-memory cache can automatically be saved to and restored from offline storage. Setting it up is as easy as wrapping your app with the `CacheProvider` widget. @@ -225,7 +224,7 @@ In your widget: // ... Query( options: QueryOptions( - documentNode: gql(readRepositories), // this is the query string you just created + document: gql(readRepositories), // this is the query string you just created variables: { 'nRepositories': 50, }, @@ -330,7 +329,7 @@ The syntax for mutations is fairly similar to that of a query. The only differen Mutation( options: MutationOptions( - documentNode: gql(addStar), // this is the mutation string you just created + document: gql(addStar), // this is the mutation string you just created // you can update the cache based on results update: (Cache cache, QueryResult result) { return cache; @@ -385,7 +384,7 @@ With a bit more context (taken from **[the complete mutation example `StarrableR // bool get optimistic => (repository as LazyCacheMap).isOptimistic; Mutation( options: MutationOptions( - documentNode: gql(starred ? mutations.removeStar : mutations.addStar), + document: gql(starred ? mutations.removeStar : mutations.addStar), // will be called for both optimistic and final results update: (Cache cache, QueryResult result) { if (result.hasException) { @@ -558,7 +557,7 @@ import 'dart:io' show File; String filePath = '/aboslute/path/to/file.ext'; final QueryResult r = await graphQLClientClient.mutate( MutationOptions( - documentNode: gql(uploadMutation), + document: gql(uploadMutation), variables: { 'files': [File(filePath)], }, @@ -572,14 +571,14 @@ This is currently our roadmap, please feel free to request additions/changes. | Feature | Progress | | :---------------------- | :------: | -| Queries | ✅ | -| Mutations | ✅ | -| Subscriptions | ✅ | -| Query polling | ✅ | -| In memory cache | ✅ | -| Offline cache sync | ✅ | -| GraphQL pload | ✅ | -| Optimistic results | ✅ | +| Queries | ✅ | +| Mutations | ✅ | +| Subscriptions | ✅ | +| Query polling | ✅ | +| In memory cache | ✅ | +| Offline cache sync | ✅ | +| GraphQL pload | ✅ | +| Optimistic results | ✅ | | Client state management | 🔜 | | Modularity | 🔜 | diff --git a/packages/graphql_flutter/example/.flutter-plugins-dependencies b/packages/graphql_flutter/example/.flutter-plugins-dependencies index 226b7206f..62ca23188 100644 --- a/packages/graphql_flutter/example/.flutter-plugins-dependencies +++ b/packages/graphql_flutter/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"connectivity","dependencies":[]},{"name":"path_provider","dependencies":[]}]} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity-0.4.8+2/","dependencies":[]},{"name":"path_provider","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.5/","dependencies":[]}],"android":[{"name":"connectivity","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity-0.4.8+2/","dependencies":[]},{"name":"path_provider","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.5/","dependencies":[]}],"macos":[{"name":"connectivity_macos","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_macos-0.1.0+2/","dependencies":[]},{"name":"path_provider_macos","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4/","dependencies":[]}],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"connectivity","dependencies":["connectivity_macos"]},{"name":"connectivity_macos","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos"]},{"name":"path_provider_macos","dependencies":[]}],"date_created":"2020-05-09 10:58:23.206186","version":"1.17.0"} \ No newline at end of file diff --git a/packages/graphql_flutter/example/lib/fetchmore/main.dart b/packages/graphql_flutter/example/lib/fetchmore/main.dart index f9c18e5ce..f37584e72 100644 --- a/packages/graphql_flutter/example/lib/fetchmore/main.dart +++ b/packages/graphql_flutter/example/lib/fetchmore/main.dart @@ -82,7 +82,7 @@ class _MyHomePageState extends State { ), Query( options: QueryOptions( - documentNode: gql(queries.searchRepositories), + document: gql(queries.searchRepositories), variables: { 'nRepositories': nRepositories, 'query': _searchQuery, diff --git a/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart b/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart index 6f62e89aa..57c43a6a0 100644 --- a/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart +++ b/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart @@ -73,7 +73,7 @@ class Bloc { Future _mutateToggleStar(Repo repo) async { final MutationOptions _options = MutationOptions( - documentNode: + document: gql(repo.viewerHasStarred ? mutations.removeStar : mutations.addStar), variables: { 'starrableId': repo.id, @@ -95,7 +95,7 @@ class Bloc { // fetchPolicy = FetchPolicy.cacheAndNetwork; // } final WatchQueryOptions _options = WatchQueryOptions( - documentNode: parseString(queries.readRepositories), + document: parseString(queries.readRepositories), variables: { 'nRepositories': nRepositories, }, diff --git a/packages/graphql_flutter/example/lib/graphql_widget/main.dart b/packages/graphql_flutter/example/lib/graphql_widget/main.dart index 0047c8234..253de5080 100644 --- a/packages/graphql_flutter/example/lib/graphql_widget/main.dart +++ b/packages/graphql_flutter/example/lib/graphql_widget/main.dart @@ -95,7 +95,7 @@ class _MyHomePageState extends State { ), Query( options: QueryOptions( - documentNode: gql(queries.readRepositories), + document: gql(queries.readRepositories), variables: { 'nRepositories': nRepositories, }, @@ -174,7 +174,7 @@ class StarrableRepository extends StatelessWidget { Widget build(BuildContext context) { return Mutation( options: MutationOptions( - documentNode: gql(starred ? mutations.removeStar : mutations.addStar), + document: gql(starred ? mutations.removeStar : mutations.addStar), update: (Cache cache, QueryResult result) { if (result.hasException) { print(result.exception); diff --git a/packages/graphql_flutter/lib/src/widgets/subscription.dart b/packages/graphql_flutter/lib/src/widgets/subscription.dart index 39e9c5900..00efdf854 100644 --- a/packages/graphql_flutter/lib/src/widgets/subscription.dart +++ b/packages/graphql_flutter/lib/src/widgets/subscription.dart @@ -44,6 +44,7 @@ class _SubscriptionState extends State> { T _data; dynamic _error; StreamSubscription _subscription; + GraphQLClient _client; ConnectivityResult _currentConnectivityResult; StreamSubscription _networkSubscription; @@ -59,7 +60,7 @@ class _SubscriptionState extends State> { variables: widget.variables, ); - final Stream stream = client.subscribe(request); + final Stream stream = _client.subscribe(request); if (_subscription == null) { // Set the initial value for the first time. diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index f26bfcb4a..d8649b9a3 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -1,11 +1,12 @@ name: graphql_flutter -description: A GraphQL client for Flutter, bringing all the features from a modern +description: + A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. version: 3.1.0-beta.4 authors: -- Eus Dima -- Zino Hofmann -- Michael Joseph Rosenthal + - Eus Dima + - Zino Hofmann + - Michael Joseph Rosenthal homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: graphql: ^3.0.1-beta.2 @@ -22,4 +23,8 @@ dev_dependencies: sdk: flutter test: ^1.5.3 environment: - sdk: '>=2.6.0 <3.0.0' + sdk: ">=2.6.0 <3.0.0" + +dependency_overrides: + graphql: + path: ../graphql diff --git a/packages/graphql_flutter/test/widgets/query_test.dart b/packages/graphql_flutter/test/widgets/query_test.dart index a727dc1c8..a92e33678 100644 --- a/packages/graphql_flutter/test/widgets/query_test.dart +++ b/packages/graphql_flutter/test/widgets/query_test.dart @@ -23,7 +23,7 @@ class Page extends StatefulWidget { this.variables, this.fetchPolicy, this.errorPolicy, - }): super(key: key); + }) : super(key: key); @override State createState() => PageState(); @@ -64,15 +64,13 @@ class PageState extends State { Widget build(BuildContext context) { return Query( options: QueryOptions( - documentNode: query, + document: query, variables: variables, fetchPolicy: fetchPolicy, errorPolicy: errorPolicy, ), - builder: (QueryResult result, { - Refetch refetch, - FetchMore fetchMore - }) => Container(), + builder: (QueryResult result, {Refetch refetch, FetchMore fetchMore}) => + Container(), ); } } @@ -159,8 +157,7 @@ void main() { verify(mockHttpClient.send(any)).called(1); - tester.state(find.byWidget(page)) - .setVariables({'foo': 2}); + tester.state(find.byWidget(page)).setVariables({'foo': 2}); await tester.pump(); verify(mockHttpClient.send(any)).called(1); }); @@ -178,8 +175,9 @@ void main() { verify(mockHttpClient.send(any)).called(1); - tester.state(find.byWidget(page)) - .setFetchPolicy(FetchPolicy.cacheFirst); + tester + .state(find.byWidget(page)) + .setFetchPolicy(FetchPolicy.cacheFirst); await tester.pump(); verify(mockHttpClient.send(any)).called(1); }); @@ -197,13 +195,15 @@ void main() { verify(mockHttpClient.send(any)).called(1); - tester.state(find.byWidget(page)) - .setErrorPolicy(ErrorPolicy.none); + tester + .state(find.byWidget(page)) + .setErrorPolicy(ErrorPolicy.none); await tester.pump(); verify(mockHttpClient.send(any)).called(1); }); - testWidgets('does not issues new network request when policies are effectively unchanged', + testWidgets( + 'does not issues new network request when policies are effectively unchanged', (WidgetTester tester) async { final page = Page( fetchPolicy: FetchPolicy.cacheAndNetwork, From f54c6aeba316a263dc42c3c53ea2a59a14a747aa Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 9 May 2020 12:01:49 -0500 Subject: [PATCH 004/118] fix: query test --- .../example/.flutter-plugins-dependencies | 1 - .../test/widgets/query_test.dart | 72 ++++++++++++++++--- 2 files changed, 62 insertions(+), 11 deletions(-) delete mode 100644 packages/graphql_flutter/example/.flutter-plugins-dependencies diff --git a/packages/graphql_flutter/example/.flutter-plugins-dependencies b/packages/graphql_flutter/example/.flutter-plugins-dependencies deleted file mode 100644 index 62ca23188..000000000 --- a/packages/graphql_flutter/example/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity-0.4.8+2/","dependencies":[]},{"name":"path_provider","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.5/","dependencies":[]}],"android":[{"name":"connectivity","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity-0.4.8+2/","dependencies":[]},{"name":"path_provider","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.5/","dependencies":[]}],"macos":[{"name":"connectivity_macos","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_macos-0.1.0+2/","dependencies":[]},{"name":"path_provider_macos","path":"/Users/mjr/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4/","dependencies":[]}],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"connectivity","dependencies":["connectivity_macos"]},{"name":"connectivity_macos","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos"]},{"name":"path_provider_macos","dependencies":[]}],"date_created":"2020-05-09 10:58:23.206186","version":"1.17.0"} \ No newline at end of file diff --git a/packages/graphql_flutter/test/widgets/query_test.dart b/packages/graphql_flutter/test/widgets/query_test.dart index a92e33678..0b5634469 100644 --- a/packages/graphql_flutter/test/widgets/query_test.dart +++ b/packages/graphql_flutter/test/widgets/query_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; + import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:graphql_flutter/src/widgets/query.dart'; import 'package:http/http.dart'; @@ -84,7 +85,7 @@ void main() { setUp(() async { mockHttpClient = MockHttpClient(); httpLink = HttpLink( - uri: 'https://unused/graphql', + 'https://unused/graphql', httpClient: mockHttpClient, ); client = ValueNotifier( @@ -110,7 +111,13 @@ void main() { child: page, )); - verify(mockHttpClient.send(any)).called(1); + verify( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).called(1); tester.state(find.byWidget(page)) ..setVariables({'foo': 1}) @@ -133,7 +140,13 @@ void main() { child: page, )); - verify(mockHttpClient.send(any)).called(1); + verify( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).called(1); tester.state(find.byWidget(page)) ..setFetchPolicy(null) @@ -155,11 +168,20 @@ void main() { child: page, )); - verify(mockHttpClient.send(any)).called(1); + verify( + mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body')), + ).called(1); tester.state(find.byWidget(page)).setVariables({'foo': 2}); await tester.pump(); - verify(mockHttpClient.send(any)).called(1); + verify( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).called(1); }); testWidgets('issues a new network request when fetch policy changes', @@ -173,13 +195,25 @@ void main() { child: page, )); - verify(mockHttpClient.send(any)).called(1); + verify( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).called(1); tester .state(find.byWidget(page)) .setFetchPolicy(FetchPolicy.cacheFirst); await tester.pump(); - verify(mockHttpClient.send(any)).called(1); + verify( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).called(1); }); testWidgets('issues a new network request when error policy changes', @@ -193,13 +227,25 @@ void main() { child: page, )); - verify(mockHttpClient.send(any)).called(1); + verify( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).called(1); tester .state(find.byWidget(page)) .setErrorPolicy(ErrorPolicy.none); await tester.pump(); - verify(mockHttpClient.send(any)).called(1); + verify( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).called(1); }); testWidgets( @@ -215,7 +261,13 @@ void main() { child: page, )); - verify(mockHttpClient.send(any)).called(1); + verify( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).called(1); tester.state(find.byWidget(page)) ..setFetchPolicy(null) From 951b45d7bd6df6bf097f8d6dc7fb486e0ccc56a1 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 9 May 2020 16:41:38 -0500 Subject: [PATCH 005/118] borrow some links from gql --- examples/flutter_bloc/lib/main.dart | 4 +- examples/flutter_bloc/pubspec.yaml | 1 - packages/graphql/lib/client.dart | 1 + packages/graphql/lib/src/links/auth_link.dart | 69 +++++++++++++++++ .../graphql/lib/src/links/error_link.dart | 77 +++++++++++++++++++ packages/graphql/pubspec.yaml | 1 + packages/graphql_flutter/example/pubspec.yaml | 4 + 7 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 packages/graphql/lib/src/links/auth_link.dart create mode 100644 packages/graphql/lib/src/links/error_link.dart diff --git a/examples/flutter_bloc/lib/main.dart b/examples/flutter_bloc/lib/main.dart index 86c0d4ee8..c40a2163e 100644 --- a/examples/flutter_bloc/lib/main.dart +++ b/examples/flutter_bloc/lib/main.dart @@ -44,9 +44,7 @@ class MyApp extends StatelessWidget { } GraphQLClient _client() { - final HttpLink _httpLink = HttpLink( - uri: 'https://api.github.com/graphql', - ); + final HttpLink _httpLink = HttpLink('https://api.github.com/graphql'); final AuthLink _authLink = AuthLink( getToken: () => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', diff --git a/examples/flutter_bloc/pubspec.yaml b/examples/flutter_bloc/pubspec.yaml index 344fe077f..a32c0d414 100644 --- a/examples/flutter_bloc/pubspec.yaml +++ b/examples/flutter_bloc/pubspec.yaml @@ -9,7 +9,6 @@ environment: dependencies: flutter: sdk: flutter - graphql: path: ../../packages/graphql cupertino_icons: ^0.1.2 diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index d77237fdf..6e2e66c10 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -11,5 +11,6 @@ export 'package:graphql/src/core/query_result.dart'; export 'package:graphql/src/exceptions/exceptions.dart'; export 'package:graphql/src/graphql_client.dart'; +export 'package:graphql/src/links/auth_link.dart'; export 'package:gql_link/gql_link.dart'; export 'package:gql_http_link/gql_http_link.dart'; diff --git a/packages/graphql/lib/src/links/auth_link.dart b/packages/graphql/lib/src/links/auth_link.dart new file mode 100644 index 000000000..71653e803 --- /dev/null +++ b/packages/graphql/lib/src/links/auth_link.dart @@ -0,0 +1,69 @@ +import 'dart:async'; +import 'package:meta/meta.dart'; + +import "package:gql_exec/gql_exec.dart"; +import "package:gql_http_link/gql_http_link.dart"; +import "package:gql_link/gql_link.dart"; +import "package:gql_transform_link/gql_transform_link.dart"; + +import "./error_link.dart"; + +// TODO temporarily taken from gql https://github.com/gql-dart/gql/pull/103 +class AuthLink extends Link { + Link _link; + String _token; + + final FutureOr Function() getToken; + + final String headerKey; + + AuthLink({ + @required this.getToken, + this.headerKey = 'Authorization', + }) { + _link = Link.concat( + ErrorLink(onException: handleException), + TransformLink(requestTransformer: transformRequest), + ); + } + + Future updateToken() async { + _token = await getToken(); + } + + Stream handleException( + Request request, + NextLink forward, + LinkException exception, + ) async* { + if (exception is HttpLinkServerException && + exception.response.statusCode == 401) { + await updateToken(); + + yield* forward(request); + + return; + } + + throw exception; + } + + Request transformRequest(Request request) => + request.updateContextEntry( + (headers) => HttpLinkHeaders( + headers: { + ...headers?.headers ?? {}, + headerKey: _token, + }, + ), + ); + + @override + Stream request(Request request, [forward]) async* { + if (_token == null) { + await updateToken(); + } + + yield* _link.request(request, forward); + } +} diff --git a/packages/graphql/lib/src/links/error_link.dart b/packages/graphql/lib/src/links/error_link.dart new file mode 100644 index 000000000..a029d5e7e --- /dev/null +++ b/packages/graphql/lib/src/links/error_link.dart @@ -0,0 +1,77 @@ +// TODO temporarily taken from gql https://github.com/gql-dart/gql/pull/103 +import "dart:async"; +import "package:async/async.dart"; +import "package:gql_link/gql_link.dart"; +import "package:gql_exec/gql_exec.dart"; + +/// A handler of GraphQL errors. +typedef ErrorHandler = Stream Function( + Request request, + NextLink forward, + Response response, +); + +/// A handler of Link Exceptions. +typedef ExceptionHandler = Stream Function( + Request request, + NextLink forward, + LinkException exception, +); + +/// [ErrorLink] allows interception of GraphQL errors (using [onGraphQLError]) +/// and [LinkException]s (using [onException]). +/// +/// In both cases [ErrorLink] transfers control over to the handler which may +/// return a new stream to discard the original stream. If the handler returns +/// `null`, the original stream is left intact and will be allowed to continue +/// streaming new events. +class ErrorLink extends Link { + final ErrorHandler onGraphQLError; + final ExceptionHandler onException; + + const ErrorLink({ + this.onGraphQLError, + this.onException, + }); + + @override + Stream request( + Request request, [ + forward, + ]) async* { + await for (final result in Result.captureStream(forward(request))) { + if (result.isError) { + final error = result.asError.error; + + if (onException != null && error is LinkException) { + final stream = onException(request, forward, error); + + if (stream != null) { + yield* stream; + + return; + } + } + + yield* Stream.error(error); + } + + if (result.isValue) { + final response = result.asValue.value; + final errors = response.errors; + + if (onGraphQLError != null && errors != null && errors.isNotEmpty) { + final stream = onGraphQLError(request, forward, response); + + if (stream != null) { + yield* stream; + + return; + } + } + + yield response; + } + } + } +} diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 8bd63e96f..042eb9c49 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: gql_exec: ^0.2.2 gql_link: ^0.3.0 gql_http_link: ^0.2.9 + gql_transform_link: ^0.1.5 quiver: ">=2.0.0 <3.0.0" dev_dependencies: pedantic: ^1.8.0+1 diff --git a/packages/graphql_flutter/example/pubspec.yaml b/packages/graphql_flutter/example/pubspec.yaml index 144d48611..d36555a1b 100644 --- a/packages/graphql_flutter/example/pubspec.yaml +++ b/packages/graphql_flutter/example/pubspec.yaml @@ -21,3 +21,7 @@ dev_dependencies: flutter: uses-material-design: true + +dependency_overrides: + graphql: + path: ../../graphql From 2ed7d8235f6b8a80bc3cbece2ab984add8f743c4 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 9 May 2020 16:57:46 -0500 Subject: [PATCH 006/118] cleanup examples a bit --- packages/graphql/example/bin/main.dart | 2 ++ packages/graphql_flutter/example/lib/fetchmore/main.dart | 4 +--- packages/graphql_flutter/example/lib/graphql_widget/main.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/graphql/example/bin/main.dart b/packages/graphql/example/bin/main.dart index 24ce9ba47..4e994cf35 100644 --- a/packages/graphql/example/bin/main.dart +++ b/packages/graphql/example/bin/main.dart @@ -1,6 +1,8 @@ import 'dart:io' show stdout, stderr, exit; import 'package:args/args.dart'; +import 'package:gql_http_link/gql_http_link.dart'; +import 'package:gql_link/gql_link.dart'; import 'package:graphql/client.dart'; import './graphql_operation/mutations/mutations.dart'; diff --git a/packages/graphql_flutter/example/lib/fetchmore/main.dart b/packages/graphql_flutter/example/lib/fetchmore/main.dart index d63eb7a66..082f15462 100644 --- a/packages/graphql_flutter/example/lib/fetchmore/main.dart +++ b/packages/graphql_flutter/example/lib/fetchmore/main.dart @@ -11,9 +11,7 @@ class FetchMoreWidgetScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final httpLink = HttpLink( - uri: 'https://api.github.com/graphql', - ); + final httpLink = HttpLink('https://api.github.com/graphql'); final authLink = AuthLink( // ignore: undefined_identifier diff --git a/packages/graphql_flutter/example/lib/graphql_widget/main.dart b/packages/graphql_flutter/example/lib/graphql_widget/main.dart index 0236779cf..63bd8f0a4 100644 --- a/packages/graphql_flutter/example/lib/graphql_widget/main.dart +++ b/packages/graphql_flutter/example/lib/graphql_widget/main.dart @@ -16,7 +16,7 @@ class GraphQLWidgetScreen extends StatelessWidget { @override Widget build(BuildContext context) { final httpLink = HttpLink( - uri: 'https://api.github.com/graphql', + 'https://api.github.com/graphql', ); final authLink = AuthLink( From 4f7b424cc6eab37ecc2a1a74d1d5a0887cd67b0d Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 13 May 2020 17:27:58 -0500 Subject: [PATCH 007/118] about to revert Request extension api idea --- packages/graphql/lib/src/cache/cache.dart | 52 ++++++++++ .../graphql/lib/src/cache/data_proxy.dart | 95 +++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 packages/graphql/lib/src/cache/data_proxy.dart diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index e405eeb56..1947f5712 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -1,3 +1,10 @@ +import "package:meta/meta.dart"; + +import 'package:gql_exec/gql_exec.dart' show Request; +import 'package:gql/ast.dart' show DocumentNode; + +import './data_proxy.dart'; + abstract class Cache { dynamic read(String key) {} @@ -12,3 +19,48 @@ abstract class Cache { void reset() {} } + +class ReadRequest extends Request { + /// The root query id to read from the store + /// + /// defaults to the root query of the graphql schema + String rootId; + + /// Whether to include optimistic results + bool optimistic; + + /// Previous result of this query, if any + // dynamic previousResult; + +} + +class WriteRequest extends Request { + /// The data id to read from the store + String dataId; + + /// Whether to write as an optimistic patch + bool optimistic; + + /// Result to write + dynamic result; +} + +// Restore, reset, extract should be on store + +abstract class GrahpQLCache extends GraphQLDataProxy { + // required to implement + // core API + dynamic read(ReadRequest request); + + void write(WriteRequest request); + + ///If called with only one argument, removes the entire entity + /// identified by dataId. + /// + /// If called with a [fieldName] as well, removes all + /// fields of the identified entity whose store names match fieldName. + bool evict(String dataId, [String fieldName]); + + // optimistic API + void removeOptimisticPatch(String id); +} diff --git a/packages/graphql/lib/src/cache/data_proxy.dart b/packages/graphql/lib/src/cache/data_proxy.dart new file mode 100644 index 000000000..e1f1b336f --- /dev/null +++ b/packages/graphql/lib/src/cache/data_proxy.dart @@ -0,0 +1,95 @@ +import "package:meta/meta.dart"; + +import 'package:gql_exec/gql_exec.dart' show Request; +import 'package:gql/ast.dart' show DocumentNode; + +class ReadFragmentRequest extends Request { + /** + * The root id to be used. This id should take the same form as the + * value returned by your `dataIdFromObject` function. If a value with your + * id does not exist in the store, `null` will be returned. + */ + String id; + + /** + * A GraphQL document created using the `gql` template string tag from + * `graphql-tag` with one or more fragments which will be used to determine + * the shape of data to read. If you provide more than one fragment in this + * document then you must also specify `fragmentName` to select a single. + */ + DocumentNode fragment; + + /** + * The name of the fragment in your GraphQL document to be used. If you do + * not provide a `fragmentName` and there is only one fragment in your + * `fragment` document then that fragment will be used. + */ + String fragmentName; + + /** + * Any variables that your GraphQL fragments depend on. + */ + Map variables; +} + +class WriteRequest extends Request { + /** + * The data you will be writing to the store. + */ + dynamic data; +} + +class WriteFragmentRequest extends ReadFragmentRequest implements WriteRequest { + /** + * The data you will be writing to the store. + */ + dynamic data; +} + +/// A proxy to the normalized data living in our store. +/// +/// This interface allows a user to read and write +/// denormalized data which feels natural to the user +/// whilst in the background this data is being converted +/// into the normalized store format. +abstract class GraphQLDataProxy { + /// Reads a GraphQL query from the root query id. + Map readQuery(Request request, {bool optimistic}); + + /// Reads a GraphQL fragment from any arbitrary id. + /// + /// If there is more than one fragment in the provided document + /// then a `fragmentName` must be provided to select the correct fragment. + // TODO the request extension api should have an idFields converter + Map readFragment( + ReadFragmentRequest request, { + bool optimistic, + }); + + /// Writes a GraphQL query to the root query id. + /// + /// [normalize] the given `data` into the cache using graphql metadata from `request` + /// + /// Conceptually, this can be thought of as providing a manual execution result + /// in the form of `data` + void writeQuery( + WriteRequest request, + Map data, { + bool optimistic = false, + String queryId, + }); + + /// Writes a GraphQL fragment to any arbitrary id. + /// + /// If there is more than one fragment in the provided document + /// then a `fragmentName` must be provided to select the correct fragment. + void writeFragment({ + @required DocumentNode fragment, + @required Map idFields, + @required Map data, + String fragmentName, + Map variables, + bool optimistic = false, + String queryId, + }); +} From e9b56606c45f4616db75ec7cfc0918a6f0419a12 Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 13 May 2020 19:53:58 -0500 Subject: [PATCH 008/118] feat(graphql): complete caching overhaul --- packages/graphql/lib/src/cache/cache.dart | 174 +++++++++++----- .../graphql/lib/src/cache/data_proxy.dart | 55 +---- packages/graphql/lib/src/cache/in_memory.dart | 4 - .../graphql/lib/src/cache/in_memory_html.dart | 85 -------- .../graphql/lib/src/cache/in_memory_io.dart | 143 ------------- .../graphql/lib/src/cache/in_memory_stub.dart | 29 --- .../graphql/lib/src/cache/lazy_cache_map.dart | 192 ------------------ .../lib/src/cache/normalized_in_memory.dart | 185 ----------------- .../lib/src/cache/normalizing_data_proxy.dart | 90 ++++++++ .../graphql/lib/src/cache/optimistic.dart | 155 -------------- packages/graphql/lib/src/cache/store.dart | 45 ++++ 11 files changed, 265 insertions(+), 892 deletions(-) delete mode 100644 packages/graphql/lib/src/cache/in_memory.dart delete mode 100644 packages/graphql/lib/src/cache/in_memory_html.dart delete mode 100644 packages/graphql/lib/src/cache/in_memory_io.dart delete mode 100644 packages/graphql/lib/src/cache/in_memory_stub.dart delete mode 100644 packages/graphql/lib/src/cache/lazy_cache_map.dart delete mode 100644 packages/graphql/lib/src/cache/normalized_in_memory.dart create mode 100644 packages/graphql/lib/src/cache/normalizing_data_proxy.dart delete mode 100644 packages/graphql/lib/src/cache/optimistic.dart create mode 100644 packages/graphql/lib/src/cache/store.dart diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index 1947f5712..bc1220e62 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -1,66 +1,140 @@ -import "package:meta/meta.dart"; +import 'dart:collection'; -import 'package:gql_exec/gql_exec.dart' show Request; -import 'package:gql/ast.dart' show DocumentNode; +import 'package:graphql/src/cache/normalizing_data_proxy.dart'; +import 'package:meta/meta.dart'; -import './data_proxy.dart'; +import 'package:graphql/src/cache/data_proxy.dart'; -abstract class Cache { - dynamic read(String key) {} +import 'package:graphql/src/utilities/helpers.dart'; +import 'package:graphql/src/cache/store.dart'; - void write( - String key, - dynamic value, - ) {} +export './data_proxy.dart'; - Future save() async {} +typedef CacheTransaction = GraphQLDataProxy Function(GraphQLDataProxy proxy); - void restore() {} - - void reset() {} +class OptimisticPatch extends Object { + OptimisticPatch(this.id, this.data); + String id; + HashMap data; } -class ReadRequest extends Request { - /// The root query id to read from the store - /// - /// defaults to the root query of the graphql schema - String rootId; - - /// Whether to include optimistic results - bool optimistic; +class OptimisticProxy extends NormalizingDataProxy { + OptimisticProxy(this.cache); - /// Previous result of this query, if any - // dynamic previousResult; + GraphQLCache cache; -} - -class WriteRequest extends Request { - /// The data id to read from the store - String dataId; + HashMap data = HashMap(); - /// Whether to write as an optimistic patch - bool optimistic; + @override + dynamic read(String rootId, {bool optimistic = true}) { + if (!optimistic) { + return cache.read(rootId, optimistic: false); + } + // the cache calls `patch.data.containsKey(rootId)`, + // so this is not an infinite loop + return data[rootId] ?? cache.read(rootId, optimistic: true); + } - /// Result to write - dynamic result; + @override + void write(String dataId, dynamic value) => data[dataId] = value; } -// Restore, reset, extract should be on store - -abstract class GrahpQLCache extends GraphQLDataProxy { - // required to implement - // core API - dynamic read(ReadRequest request); - - void write(WriteRequest request); - - ///If called with only one argument, removes the entire entity - /// identified by dataId. +class GraphQLCache extends NormalizingDataProxy { + GraphQLCache({ + Store store, + this.dataIdFromObject, + }) : store = store ?? InMemoryStore(); + + @protected + final Store store; + + final DataIdResolver dataIdFromObject; + + @protected + List optimisticPatches = []; + + /// Reads and dereferences an entity from the first valid optimistic layer, + /// defaulting to the base internal HashMap. + Object read(String rootId, {bool optimistic = true}) { + Object value = store.get(rootId); + + if (!optimistic) { + return value; + } + + for (OptimisticPatch patch in optimisticPatches) { + if (patch.data.containsKey(rootId)) { + final Object patchData = patch.data[rootId]; + if (value is Map && patchData is Map) { + value = deeplyMergeLeft([ + value as Map, + patchData, + ]); + } else { + // Overwrite if not mergable + value = patchData; + } + } + } + return value; + } + + void write(String dataId, dynamic value) => store.put(dataId, value); + + OptimisticProxy get _proxy => OptimisticProxy(this); + + String _parentPatchId(String id) { + final List parts = id.split('.'); + if (parts.length > 1) { + return parts.first; + } + return null; + } + + bool _patchExistsFor(String id) => + optimisticPatches.firstWhere( + (OptimisticPatch patch) => patch.id == id, + orElse: () => null, + ) != + null; + + /// avoid race conditions from slow updates /// - /// If called with a [fieldName] as well, removes all - /// fields of the identified entity whose store names match fieldName. - bool evict(String dataId, [String fieldName]); - - // optimistic API - void removeOptimisticPatch(String id); + /// if a server result is returned before an optimistic update is finished, + /// that update is discarded + bool _safeToAdd(String id) { + final String parentId = _parentPatchId(id); + return parentId == null || _patchExistsFor(parentId); + } + + /// Add a given patch using the given [transform] + /// + /// 1 level of hierarchical optimism is supported: + /// * if a patch has the id `$queryId.child`, it will be removed with `$queryId` + /// * if the update somehow fails to complete before the root response is removed, + /// It will still be called, but the result will not be added. + /// + /// This allows for multiple optimistic treatments of a query, + /// without having to tightly couple optimistic changes + void recordOptimisticTransaction( + CacheTransaction transaction, + String addId, + ) { + final OptimisticProxy patch = transaction(_proxy) as OptimisticProxy; + if (_safeToAdd(addId)) { + optimisticPatches.add(OptimisticPatch(addId, patch.data)); + } + } + + /// Remove a given patch from the list + /// + /// This will also remove all "nested" patches, such as `$queryId.update` + /// This allows for hierarchical optimism that is automatically cleaned up + /// without having to tightly couple optimistic changes + void removeOptimisticPatch(String removeId) { + optimisticPatches.removeWhere( + (OptimisticPatch patch) => + patch.id == removeId || _parentPatchId(patch.id) == removeId, + ); + } } diff --git a/packages/graphql/lib/src/cache/data_proxy.dart b/packages/graphql/lib/src/cache/data_proxy.dart index e1f1b336f..2c1c0b9ea 100644 --- a/packages/graphql/lib/src/cache/data_proxy.dart +++ b/packages/graphql/lib/src/cache/data_proxy.dart @@ -3,49 +3,6 @@ import "package:meta/meta.dart"; import 'package:gql_exec/gql_exec.dart' show Request; import 'package:gql/ast.dart' show DocumentNode; -class ReadFragmentRequest extends Request { - /** - * The root id to be used. This id should take the same form as the - * value returned by your `dataIdFromObject` function. If a value with your - * id does not exist in the store, `null` will be returned. - */ - String id; - - /** - * A GraphQL document created using the `gql` template string tag from - * `graphql-tag` with one or more fragments which will be used to determine - * the shape of data to read. If you provide more than one fragment in this - * document then you must also specify `fragmentName` to select a single. - */ - DocumentNode fragment; - - /** - * The name of the fragment in your GraphQL document to be used. If you do - * not provide a `fragmentName` and there is only one fragment in your - * `fragment` document then that fragment will be used. - */ - String fragmentName; - - /** - * Any variables that your GraphQL fragments depend on. - */ - Map variables; -} - -class WriteRequest extends Request { - /** - * The data you will be writing to the store. - */ - dynamic data; -} - -class WriteFragmentRequest extends ReadFragmentRequest implements WriteRequest { - /** - * The data you will be writing to the store. - */ - dynamic data; -} - /// A proxy to the normalized data living in our store. /// /// This interface allows a user to read and write @@ -60,9 +17,11 @@ abstract class GraphQLDataProxy { /// /// If there is more than one fragment in the provided document /// then a `fragmentName` must be provided to select the correct fragment. - // TODO the request extension api should have an idFields converter - Map readFragment( - ReadFragmentRequest request, { + Map readFragment({ + @required DocumentNode fragment, + @required Map idFields, + String fragmentName, + Map variables, bool optimistic, }); @@ -73,9 +32,8 @@ abstract class GraphQLDataProxy { /// Conceptually, this can be thought of as providing a manual execution result /// in the form of `data` void writeQuery( - WriteRequest request, + Request request, Map data, { - bool optimistic = false, String queryId, }); @@ -89,7 +47,6 @@ abstract class GraphQLDataProxy { @required Map data, String fragmentName, Map variables, - bool optimistic = false, String queryId, }); } diff --git a/packages/graphql/lib/src/cache/in_memory.dart b/packages/graphql/lib/src/cache/in_memory.dart deleted file mode 100644 index 0d67b1636..000000000 --- a/packages/graphql/lib/src/cache/in_memory.dart +++ /dev/null @@ -1,4 +0,0 @@ -// @todo refactor this with other in_memory_* files -export './in_memory_stub.dart' - if (dart.library.html) './in_memory_html.dart' - if (dart.library.io) './in_memory_io.dart'; diff --git a/packages/graphql/lib/src/cache/in_memory_html.dart b/packages/graphql/lib/src/cache/in_memory_html.dart deleted file mode 100644 index 7fd822efb..000000000 --- a/packages/graphql/lib/src/cache/in_memory_html.dart +++ /dev/null @@ -1,85 +0,0 @@ -// @todo refactor this with other in_memory_* files -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:html' show window; - -import 'package:meta/meta.dart'; - -import 'package:graphql/src/cache/cache.dart'; -import 'package:graphql/src/utilities/helpers.dart' show deeplyMergeLeft; - -class InMemoryCache implements Cache { - InMemoryCache({ - this.storagePrefix = '', - }) { - masterKey = storagePrefix.toString() ?? '_graphql_cache'; - } - - final FutureOr storagePrefix; - String masterKey; - - @protected - HashMap data = HashMap(); - - /// Reads an entity from the internal HashMap. - @override - dynamic read(String key) { - if (data.containsKey(key)) { - return data[key]; - } - - return null; - } - - /// Writes an entity to the internal HashMap. - @override - void write(String key, dynamic value) { - if (data.containsKey(key) && - data[key] is Map && - value != null && - value is Map) { - // Avoid overriding a superset with a subset of a field (#155) - data[key] = deeplyMergeLeft(>[ - data[key] as Map, - value, - ]); - } else { - data[key] = value; - } - } - - /// Saves the internal HashMap to a file. - @override - Future save() async { - await _writeToStorage(); - } - - /// Restores the internal HashMap to a file. - @override - Future restore() async { - data = await _readFromStorage(); - } - - /// Clears the internal HashMap. - @override - void reset() { - data.clear(); - } - - Future _writeToStorage() async { - window.localStorage[masterKey] = jsonEncode(data); - } - - Future> _readFromStorage() async { - try { - final decoded = jsonDecode(window.localStorage[masterKey]); - return HashMap.from(decoded); - } catch (error) { - // TODO: handle error - print(error); - - return HashMap(); - } - } -} diff --git a/packages/graphql/lib/src/cache/in_memory_io.dart b/packages/graphql/lib/src/cache/in_memory_io.dart deleted file mode 100644 index 58d712da7..000000000 --- a/packages/graphql/lib/src/cache/in_memory_io.dart +++ /dev/null @@ -1,143 +0,0 @@ -// @todo refactor this with other in_memory_* files -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:meta/meta.dart'; -// TODO need to think about this -// import 'package:path_provider/path_provider.dart'; - -import 'package:graphql/src/cache/cache.dart'; -import 'package:graphql/src/utilities/helpers.dart' show deeplyMergeLeft; -import 'package:path/path.dart'; - -class InMemoryCache implements Cache { - InMemoryCache({ - this.storagePrefix = '', - }); - - final FutureOr storagePrefix; - - bool _writingToStorage = false; - - @protected - HashMap data = HashMap(); - - /// Reads an entity from the internal HashMap. - @override - dynamic read(String key) { - if (data.containsKey(key)) { - return data[key]; - } - - return null; - } - - /// Writes an entity to the internal HashMap. - @override - void write(String key, dynamic value) { - if (data.containsKey(key) && - data[key] is Map && - value != null && - value is Map) { - // Avoid overriding a superset with a subset of a field (#155) - data[key] = deeplyMergeLeft(>[ - data[key] as Map, - value, - ]); - } else { - data[key] = value; - } - } - - /// Saves the internal HashMap to a file. - @override - Future save() async { - await _writeToStorage(); - } - - /// Restores the internal HashMap to a file. - @override - Future restore() async { - data = await _readFromStorage(); - } - - /// Clears the internal HashMap. - @override - void reset() { - data.clear(); - } - - FutureOr get _localStorageFile async { - return File(join(await storagePrefix, 'cache.txt')); - } - - Future _writeToStorage() async { - if (_writingToStorage) { - return; - } - - _writingToStorage = true; - - // Catching errors to avoid locking forever. - // Maybe the device couldn't write in the past - // but it may in the future. - try { - final File file = await _localStorageFile; - final IOSink sink = file.openWrite(); - data.forEach((String key, dynamic value) { - sink.writeln(json.encode([key, value])); - }); - - await sink.flush(); - await sink.close(); - - _writingToStorage = false; - } catch (err) { - _writingToStorage = false; - - rethrow; - } - return; - } - - /// Attempts to read saved state from the file cache `_localStorageFile`. - /// - /// Will return the current in-memory cache if writing, - /// or an empty map on failure - Future> _readFromStorage() async { - if (_writingToStorage) { - return data; - } - try { - final File file = await _localStorageFile; - final HashMap storedHashMap = HashMap(); - - if (file.existsSync()) { - final Stream> inputStream = file.openRead(); - - await for (String line in inputStream - .transform(utf8.decoder) // Decode bytes to UTF8. - .transform( - const LineSplitter(), - )) { - final List keyAndValue = json.decode(line) as List; - storedHashMap[keyAndValue[0] as String] = keyAndValue[1]; - } - } - - return storedHashMap; - } on FileSystemException { - // TODO: handle no such file - print('Can\'t read file from storage, returning an empty HashMap.'); - - return HashMap(); - } catch (error) { - // TODO: handle error - print(error); - - return HashMap(); - } - } -} diff --git a/packages/graphql/lib/src/cache/in_memory_stub.dart b/packages/graphql/lib/src/cache/in_memory_stub.dart deleted file mode 100644 index f5c4c7dee..000000000 --- a/packages/graphql/lib/src/cache/in_memory_stub.dart +++ /dev/null @@ -1,29 +0,0 @@ -// @todo refactor this with other in_memory_* files -import 'dart:async'; -import 'dart:collection'; - -import 'package:graphql/src/cache/cache.dart'; -import 'package:meta/meta.dart'; - -class InMemoryCache implements Cache { - noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); - - InMemoryCache({ - this.storagePrefix = '', - }); - - /// For web/browser, `storagePrefix` is a prefix to the key - /// for [window.localStorage] - /// - /// For vm/flutter, `storagePrefix` is a path to the directory - /// that can save `cache.txt` file - /// - /// For flutter usually provided by - /// [path_provider.getApplicationDocumentsDirectory] - /// - /// @NotNull - final FutureOr storagePrefix; - - @protected - HashMap data = HashMap(); -} diff --git a/packages/graphql/lib/src/cache/lazy_cache_map.dart b/packages/graphql/lib/src/cache/lazy_cache_map.dart deleted file mode 100644 index 23b07d105..000000000 --- a/packages/graphql/lib/src/cache/lazy_cache_map.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:core'; -import 'package:quiver/core.dart' show hash3; - -import 'package:meta/meta.dart'; - -typedef Dereference = Object Function(Object node); - -enum CacheState { OPTIMISTIC } - -/// A [LazyDereferencingMap] into the cache with added `cacheState` information -class LazyCacheMap extends LazyDereferencingMap { - LazyCacheMap( - Map data, { - @required Dereference dereference, - CacheState cacheState, - }) : cacheState = - cacheState ?? (data is LazyCacheMap ? data.cacheState : null), - super(data, dereference: dereference); - - final CacheState cacheState; - bool get isOptimistic => cacheState == CacheState.OPTIMISTIC; - - @override - Object getValue(Object value) { - final Object result = _dereference(value) ?? value; - if (result is List) { - return result.map(getValue).toList(); - } - if (result is Map) { - return LazyCacheMap( - result, - dereference: _dereference, - ); - } - return result; - } - - int get hashCode => hash3(_data, _dereference, cacheState); - - bool operator ==(Object other) => - other is LazyCacheMap && - other._data == _data && - other._dereference == _dereference && - other.cacheState == cacheState; -} - -/// Unwrap a given Object that could possibly be a lazy map -Object unwrap(Object possibleLazyMap) => possibleLazyMap is LazyDereferencingMap - ? possibleLazyMap.data - : possibleLazyMap; - -/// Unwrap a given mpa that could possibly be a lazy map -Map unwrapMap(Map possibleLazyMap) => - possibleLazyMap is LazyDereferencingMap - ? possibleLazyMap.data - : possibleLazyMap; - -/// A simple map wrapper that lazily dereferences using `dereference` -/// -/// Wrapper that calls `dereference(value)` for each value before returning, -/// replacing that `value` with the result if not `null`. -@immutable -class LazyDereferencingMap implements Map { - LazyDereferencingMap( - Map data, { - @required Dereference dereference, - }) : _data = unwrap(data) as Map, - _dereference = dereference; - - final Dereference _dereference; - - final Map _data; - - /// get the wrapped `Map` without dereferencing - Map get data => _data; - - @protected - Object getValue(Object value) { - final Object result = _dereference(value) ?? value; - // TODO maybe this should be encapsulated in a LazyList or something - if (result is List) { - return result.map(getValue).toList(); - } - if (result is Map) { - return LazyDereferencingMap( - result, - dereference: _dereference, - ); - } - return result; - } - - @override - Object operator [](Object key) => getValue(data[key]); - - Object get(Object key) => getValue(data[key]); - - @override - bool containsKey(Object key) => data.containsKey(key); - - @override - bool containsValue(Object value) => values.contains(value); - - @override - Iterable> get entries => data.entries - .map((MapEntry entry) => MapEntry( - entry.key, - getValue(entry.value), - )); - - @override - void forEach(void Function(String key, Object value) f) { - void _forEachEntry(MapEntry entry) { - f(entry.key, entry.value); - } - - entries.forEach(_forEachEntry); - } - - @override - bool get isEmpty => data.isEmpty; - - @override - bool get isNotEmpty => data.isNotEmpty; - - @override - Iterable get keys => data.keys; - - @override - int get length => data.length; - - @override - Map map( - MapEntry Function(String key, Object value) f) { - MapEntry _mapEntry(MapEntry entry) { - return f(entry.key, entry.value); - } - - return Map.fromEntries(entries.map(_mapEntry)); - } - - @override - Iterable get values => data.values.map(getValue); - - @override - void operator []=(String key, Object value) { - data[key] = unwrap(value); - } - - @override - void addAll(Map other) { - data.addAll(unwrap(other) as Map); - } - - @override - void addEntries(Iterable> entries) { - data.addEntries(entries); - } - - @override - void clear() { - data.clear(); - } - - @override - Object remove(Object key) => getValue(data.remove(key)); - - @override - void removeWhere(bool test(String key, Object value)) { - data.removeWhere(test); - } - - /// This operation is not supported by a [LazyUnmodifiableMapView]. - @override - Object putIfAbsent(String key, Object ifAbsent()) => - getValue(data.putIfAbsent(key, ifAbsent)); - - /// This operation is not supported by a [LazyUnmodifiableMapView]. - @override - Object update(String key, Object update(Object value), {Object ifAbsent()}) => - getValue(data.update(key, update, ifAbsent: ifAbsent)); - - @override - void updateAll(Object update(String key, Object value)) { - data.updateAll(update); - } - - @override - Map cast() { - throw UnsupportedError('Cannot cast a lazy cache map map'); - } -} diff --git a/packages/graphql/lib/src/cache/normalized_in_memory.dart b/packages/graphql/lib/src/cache/normalized_in_memory.dart deleted file mode 100644 index 81f7bb34e..000000000 --- a/packages/graphql/lib/src/cache/normalized_in_memory.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'dart:async'; - -import 'package:meta/meta.dart'; - -import 'package:graphql/src/utilities/traverse.dart'; -import 'package:graphql/src/utilities/helpers.dart'; -import 'package:graphql/src/cache/in_memory.dart'; -import 'package:graphql/src/cache/lazy_cache_map.dart'; -import 'package:graphql/src/exceptions/exceptions.dart' - show NormalizationException; - -typedef DataIdFromObject = String Function(Object node); - -typedef Normalizer = List Function(Object node); - -class NormalizedInMemoryCache extends InMemoryCache { - NormalizedInMemoryCache({ - @required this.dataIdFromObject, - this.prefix = '@cache/reference', - FutureOr storagePrefix, - }) : super(storagePrefix: storagePrefix); - - DataIdFromObject dataIdFromObject; - - String prefix; - - bool _isReference(Object node) => - node is List && node.length == 2 && node[0] == prefix; - - Object _dereference(Object node) { - if (node is List && _isReference(node)) { - return read(node[1] as String); - } - - return null; - } - - LazyCacheMap lazilyDenormalized( - Map data, [ - CacheState cacheState, - ]) { - return LazyCacheMap( - data, - dereference: _dereference, - cacheState: cacheState, - ); - } - - Object _denormalizingDereference(Object node) { - if (node is List && _isReference(node)) { - return denormalizedRead(node[1] as String); - } - - return null; - } - - // ~TODO~ ideally cyclical references would be noticed and replaced with null or something - // @micimize: pretty sure I implemented the above - /// eagerly dereferences all cache references. - /// *WARNING* if your system allows cyclical references, this will break - dynamic denormalizedRead(String key) { - try { - return Traversal(_denormalizingDereference).traverse(read(key)); - } catch (error) { - if (error is StackOverflowError) { - throw NormalizationException( - ''' - Denormalization failed for $key this is likely caused by a circular reference. - Please ensure dataIdFromObject returns a unique identifier for all possible entities in your system - ''', - error, - key, - ); - } - } - } - - @override - void reset() { - data.clear(); - } - - /* - Dereferences object references, - replacing them with cached instances - */ - @override - dynamic read(String key) { - final Object value = super.read(key); - return value is Map ? lazilyDenormalized(value) : value; - } - - // get a normalizer for a given target map - Normalizer _normalizerFor(Map into) { - List normalizer(Object node) { - final dataId = dataIdFromObject(node); - if (dataId != null) { - return [prefix, dataId]; - } - return null; - } - - return normalizer; - } - - // [_normalizerFor] for this cache's data - List _normalize(Object node) { - final String dataId = dataIdFromObject(node); - if (dataId != null) { - return [prefix, dataId]; - } - return null; - } - - /// Writes included objects to provided Map, - /// replacing discernable entities with references - void writeInto( - String key, - Object value, - Map into, [ - Normalizer normalizer, - ]) { - normalizer ??= _normalizerFor(into); - if (value is Map) { - final merged = _mergedWithExisting(into, key, value); - final Traversal traversal = Traversal( - normalizer, - transformSideEffect: _traversingWriteInto(into), - ); - // normalized the merged value - into[key] = traversal.traverseValues(merged); - } else { - // writing non-map data to the store is allowed, - // but there is no merging strategy - into[key] = value; - } - } - - /// Writes included objects to store, - /// replacing discernable entities with references - @override - void write(String key, Object value) { - writeInto(key, value, data, _normalize); - } -} - -String typenameDataIdFromObject(Object object) { - if (object is Map && - object.containsKey('__typename') && - object.containsKey('id')) { - return "${object['__typename']}/${object['id']}"; - } - return null; -} - -/// Writing side effect for traverse -/// -/// Essentially, we avoid problems with cyclical objects by -/// tracking seen nodes in the [Traversal], -/// and we pass this as a side effect to take advantage of that tracking -SideEffect _traversingWriteInto(Map into) { - void sideEffect(Object ref, Object value, Traversal traversal) { - final String key = (ref as List)[1]; - if (value is Map) { - final merged = _mergedWithExisting(into, key, value); - into[key] = traversal.traverseValues(merged); - } else { - // writing non-map data to the store is allowed, - // but there is no merging strategy - into[key] = value; - return; - } - } - - return sideEffect; -} - -/// get the given value merged with any pre-existing map with the same key -Map _mergedWithExisting( - Map into, String key, Map value) { - final existing = into[key]; - return (existing is Map) - ? deeplyMergeLeft([existing, value]) - : value; -} diff --git a/packages/graphql/lib/src/cache/normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/normalizing_data_proxy.dart new file mode 100644 index 000000000..74fefa126 --- /dev/null +++ b/packages/graphql/lib/src/cache/normalizing_data_proxy.dart @@ -0,0 +1,90 @@ +import "package:meta/meta.dart"; + +import 'package:gql_exec/gql_exec.dart' show Request; +import 'package:gql/ast.dart' show DocumentNode; + +import 'package:normalize/normalize.dart'; + +import './data_proxy.dart'; + +typedef DataIdResolver = String Function(Map object); + +/// Implements the core normalization api leveraged by the cache and proxy +/// +/// `read` and `write` must still be supplied by the implementing class +abstract class NormalizingDataProxy extends GraphQLDataProxy { + Map typePolicies; + bool addTypename; + + DataIdResolver dataIdFromObject; + + dynamic read(String rootId, {bool optimistic}); + + void write(String dataId, dynamic value); + + Map readQuery( + Request request, { + bool optimistic = true, + }) => + denormalize( + reader: (dataId) => read(dataId, optimistic: optimistic), + query: request.operation.document, + operationName: request.operation.operationName, + variables: request.variables, + typePolicies: typePolicies, + addTypename: addTypename, + ); + + Map readFragment({ + @required DocumentNode fragment, + @required Map idFields, + String fragmentName, + Map variables, + bool optimistic = true, + }) => + denormalizeFragment( + reader: (dataId) => read(dataId, optimistic: optimistic), + fragment: fragment, + idFields: idFields, + fragmentName: fragmentName, + variables: variables, + typePolicies: typePolicies, + addTypename: addTypename, + dataIdFromObject: dataIdFromObject, + ); + + /// [normalize] the given `data` into the cache using graphql metadata from `request` + void writeQuery( + Request request, + Map data, { + String queryId, + }) => + normalize( + writer: (dataId, value) => write(dataId, value), + query: request.operation.document, + operationName: request.operation.operationName, + variables: request.variables, + data: data, + typePolicies: typePolicies, + dataIdFromObject: dataIdFromObject, + ); + + void writeFragment({ + @required DocumentNode fragment, + @required Map idFields, + @required Map data, + String fragmentName, + Map variables, + String queryId, + }) => + normalizeFragment( + writer: (dataId, value) => write(dataId, value), + fragment: fragment, + idFields: idFields, + data: data, + fragmentName: fragmentName, + variables: variables, + typePolicies: typePolicies, + dataIdFromObject: dataIdFromObject, + ); +} diff --git a/packages/graphql/lib/src/cache/optimistic.dart b/packages/graphql/lib/src/cache/optimistic.dart deleted file mode 100644 index 510b9f039..000000000 --- a/packages/graphql/lib/src/cache/optimistic.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; - -import 'package:meta/meta.dart'; - -import 'package:graphql/src/utilities/helpers.dart'; -import 'package:graphql/src/cache/cache.dart'; -import 'package:graphql/src/cache/normalized_in_memory.dart'; -import 'package:graphql/src/cache/lazy_cache_map.dart'; - -class OptimisticPatch extends Object { - OptimisticPatch(this.id, this.data); - String id; - HashMap data; -} - -class OptimisticProxy implements Cache { - OptimisticProxy(this.cache); - OptimisticCache cache; - HashMap data = HashMap(); - - Object _dereference(Object node) { - if (node is List && node.length == 2 && node[0] == cache.prefix) { - return read(node[1] as String); - } - - return null; - } - - @override - dynamic read(String key) { - if (data.containsKey(key)) { - final Object value = data[key]; - return value is Map - ? LazyCacheMap( - value, - dereference: _dereference, - cacheState: CacheState.OPTIMISTIC, - ) - : value; - } - return cache.read(key); - } - - @override - void write(String key, dynamic value) { - cache.writeInto(key, value, data); - } - - // TODO should persistence be a seperate concern from caching - @override - Future save() async {} - @override - void restore() {} - @override - void reset() {} -} - -typedef CacheTransform = Cache Function(Cache proxy); - -class OptimisticCache extends NormalizedInMemoryCache { - OptimisticCache({ - @required DataIdFromObject dataIdFromObject, - String prefix = '@cache/reference', - FutureOr storagePrefix, - }) : super( - dataIdFromObject: dataIdFromObject, - prefix: prefix, - storagePrefix: storagePrefix, - ); - - @protected - List optimisticPatches = []; - - /// Reads and dereferences an entity from the first valid optimistic layer, - /// defaulting to the base internal HashMap. - @override - dynamic read(String key) { - Object value = super.read(key); - CacheState cacheState; - for (OptimisticPatch patch in optimisticPatches) { - if (patch.data.containsKey(key)) { - final Object patchData = patch.data[key]; - if (value is Map && patchData is Map) { - value = deeplyMergeLeft([ - value as Map, - patchData, - ]); - cacheState = CacheState.OPTIMISTIC; - } else { - // Overwrite if not mergable - value = patchData; - } - } - } - return value is Map - ? lazilyDenormalized(value, cacheState) - : value; - } - - OptimisticProxy get _proxy => OptimisticProxy(this); - - String _parentPatchId(String id) { - final List parts = id.split('.'); - if (parts.length > 1) { - return parts.first; - } - return null; - } - - bool _patchExistsFor(String id) => - optimisticPatches.firstWhere((OptimisticPatch patch) => patch.id == id, - orElse: () => null) != - null; - - /// avoid race conditions from slow updates - /// - /// if a server result is returned before an optimistic update is finished, - /// that update is discarded - bool _safeToAdd(String id) { - final String parentId = _parentPatchId(id); - return parentId == null || _patchExistsFor(parentId); - } - - /// Add a given patch using the given [transform] - /// - /// 1 level of hierarchical optimism is supported: - /// * if a patch has the id `$queryId.child`, it will be removed with `$queryId` - /// * if the update somehow fails to complete before the root response is removed, - /// It will still be called, but the result will not be added. - /// - /// This allows for multiple optimistic treatments of a query, - /// without having to tightly couple optimistic changes - void addOptimisiticPatch( - String addId, - CacheTransform transform, - ) { - final OptimisticProxy patch = transform(_proxy) as OptimisticProxy; - if (_safeToAdd(addId)) { - optimisticPatches.add(OptimisticPatch(addId, patch.data)); - } - } - - /// Remove a given patch from the list - /// - /// This will also remove all "nested" patches, such as `$queryId.update` - /// This allows for hierarchical optimism that is automatically cleaned up - /// without having to tightly couple optimistic changes - void removeOptimisticPatch(String removeId) { - optimisticPatches.removeWhere( - (OptimisticPatch patch) => - patch.id == removeId || _parentPatchId(patch.id) == removeId, - ); - } -} diff --git a/packages/graphql/lib/src/cache/store.dart b/packages/graphql/lib/src/cache/store.dart new file mode 100644 index 000000000..2f8af94b8 --- /dev/null +++ b/packages/graphql/lib/src/cache/store.dart @@ -0,0 +1,45 @@ +import 'dart:collection'; + +import 'package:meta/meta.dart'; + +@immutable +abstract class Store { + Map get(String dataId); + + void put(String dataId, Map value); + + void putAll(Map> data); + + void delete(String dataId); + + void reset(); + + Map> toMap(); +} + +@immutable +class InMemoryStore extends Store { + @protected + final Map data; + + InMemoryStore([Map data]) + : data = data ?? HashMap(); + + @override + Map get(String dataId) => data[dataId]; + + @override + void put(String dataId, Map value) => data[dataId] = value; + + @override + void putAll(Map> entries) => + data.addAll(entries); + + @override + void delete(String dataId) => data.remove(dataId); + + @override + Map> toMap() => data; + + void reset() => data.clear(); +} From f64a6c82aab878f3f828d86dcd71cf8422e038b1 Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 13 May 2020 19:57:49 -0500 Subject: [PATCH 009/118] feat(graphql): use new cache correctly everywhere else --- packages/graphql/lib/client.dart | 4 - .../lib/src/core/observable_query.dart | 5 ++ .../graphql/lib/src/core/query_manager.dart | 76 +++++++++---------- .../graphql/lib/src/core/query_options.dart | 36 ++++++--- packages/graphql/lib/src/graphql_client.dart | 2 +- .../graphql/lib/src/utilities/helpers.dart | 8 +- packages/graphql/pubspec.yaml | 1 + 7 files changed, 71 insertions(+), 61 deletions(-) diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index 6e2e66c10..5abf68e2d 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -1,10 +1,6 @@ library graphql; export 'package:graphql/src/cache/cache.dart'; -export 'package:graphql/src/cache/in_memory.dart'; -export 'package:graphql/src/cache/lazy_cache_map.dart'; -export 'package:graphql/src/cache/normalized_in_memory.dart'; -export 'package:graphql/src/cache/optimistic.dart'; export 'package:graphql/src/core/query_manager.dart'; export 'package:graphql/src/core/query_options.dart'; export 'package:graphql/src/core/query_result.dart'; diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 54d7ebc8a..79481ff08 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -161,6 +161,8 @@ class ObservableQuery { QueryResult fetchMoreResult = await queryManager.query(combinedOptions); + final request = options.asRequest; + try { // combine the query with the new query, using the function provided by the user fetchMoreResult.data = fetchMoreOptions.updateQuery( @@ -168,8 +170,10 @@ class ObservableQuery { fetchMoreResult.data, ); assert(fetchMoreResult.data != null, 'updateQuery result cannot be null'); + // stream the new results and rebuild queryManager.addQueryResult( + request, queryId, fetchMoreResult, writeToCache: true, @@ -185,6 +189,7 @@ class ObservableQuery { ); queryManager.addQueryResult( + request, queryId, latestResult, writeToCache: true, diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index da7e282da..584c569c0 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -1,17 +1,16 @@ import 'dart:async'; +import 'package:meta/meta.dart'; + import 'package:gql_exec/gql_exec.dart'; import 'package:gql_link/gql_link.dart'; + import 'package:graphql/src/cache/cache.dart'; -import 'package:graphql/src/cache/normalized_in_memory.dart' - show NormalizedInMemoryCache; -import 'package:graphql/src/cache/optimistic.dart' show OptimisticCache; import 'package:graphql/src/core/observable_query.dart'; import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/query_result.dart'; import 'package:graphql/src/exceptions/exceptions.dart'; import 'package:graphql/src/scheduler/scheduler.dart'; -import 'package:meta/meta.dart'; class QueryManager { QueryManager({ @@ -24,7 +23,7 @@ class QueryManager { } final Link link; - final Cache cache; + final GraphQLCache cache; QueryScheduler scheduler; int idCounter = 1; @@ -81,7 +80,11 @@ class QueryManager { String queryId, BaseOptions options, ) { + // create a new request to execute + final request = options.asRequest; + final QueryResult eagerResult = _resolveQueryEagerly( + request, queryId, options, ); @@ -93,26 +96,17 @@ class QueryManager { networkResult: (shouldStopAtCache(options.fetchPolicy) && !eagerResult.loading) ? null - : _resolveQueryOnNetwork(queryId, options), + : _resolveQueryOnNetwork(request, queryId, options), ); } /// Resolve the query on the network, /// negotiating any necessary cache edits / optimistic cleanup Future _resolveQueryOnNetwork( + Request request, String queryId, BaseOptions options, ) async { - // create a new request to execute - final Request request = Request( - operation: Operation( - document: options.document, - operationName: options.operationName, - ), - variables: options.variables, - context: options.context ?? Context(), - ); - Response response; QueryResult queryResult; @@ -126,10 +120,10 @@ class QueryManager { // save the data from response to the cache if (response.data != null && options.fetchPolicy != FetchPolicy.noCache) { - cache.write( - // TODO: think of an alternative to the old toKey(), - request.hashCode.toString(), + cache.writeQuery( + request, response.data, + queryId: queryId, ); } @@ -151,17 +145,13 @@ class QueryManager { } // cleanup optimistic results - cleanupOptimisticResults(queryId); - if (options.fetchPolicy != FetchPolicy.noCache && - cache is NormalizedInMemoryCache) { + cache.removeOptimisticPatch(queryId); + if (options.fetchPolicy != FetchPolicy.noCache) { // normalize results if previously written - queryResult.data = cache.read( - // TODO: think of an alternative to the old toKey(), - request.hashCode.toString(), - ); + queryResult.data = cache.readQuery(request); } - addQueryResult(queryId, queryResult); + addQueryResult(request, queryId, queryResult); return queryResult; } @@ -169,6 +159,7 @@ class QueryManager { /// Add an eager cache response to the stream if possible, /// based on `fetchPolicy` and `optimisticResults` QueryResult _resolveQueryEagerly( + Request request, String queryId, BaseOptions options, ) { @@ -179,6 +170,7 @@ class QueryManager { try { if (options.optimisticResult != null) { queryResult = _getOptimisticQueryResult( + request, queryId, cacheKey: cacheKey, optimisticResult: options.optimisticResult, @@ -189,7 +181,7 @@ class QueryManager { // we attempt to resolve the from the cache if (shouldRespondEagerlyFromCache(options.fetchPolicy) && !queryResult.optimistic) { - final dynamic data = cache.read(cacheKey); + final dynamic data = cache.readQuery(request, optimistic: false); // we only push an eager query with data if (data != null) { queryResult = QueryResult( @@ -225,7 +217,7 @@ class QueryManager { // This is undefined-ish behavior/edge case, but still better than just // ignoring a provided optimisticResult. // Would probably be better to add it ignoring the cache in such cases - addQueryResult(queryId, queryResult); + addQueryResult(request, queryId, queryResult); return queryResult; } @@ -244,15 +236,17 @@ class QueryManager { /// Add a result to the query specified by `queryId`, if it exists void addQueryResult( + Request request, String queryId, QueryResult queryResult, { bool writeToCache = false, }) { final ObservableQuery observableQuery = getQuery(queryId); if (writeToCache) { - cache.write( - observableQuery.options.toKey(), + cache.writeQuery( + request, queryResult.data, + queryId: observableQuery.options.toKey(), ); } @@ -263,18 +257,22 @@ class QueryManager { /// Create an optimstic result for the query specified by `queryId`, if it exists QueryResult _getOptimisticQueryResult( + Request request, String queryId, { @required String cacheKey, @required Object optimisticResult, }) { - assert(cache is OptimisticCache, - "can't optimisticly update non-optimistic cache"); - - (cache as OptimisticCache).addOptimisiticPatch( - queryId, (Cache cache) => cache..write(cacheKey, optimisticResult)); + cache.writeQuery( + request, + optimisticResult, + queryId: queryId, + ); final QueryResult queryResult = QueryResult( - data: cache.read(cacheKey), + data: cache.readQuery( + request, + optimistic: true, + ), source: QueryResultSource.OptimisticResult, ); return queryResult; @@ -282,9 +280,7 @@ class QueryManager { /// Remove the optimistic patch for `cacheKey`, if any void cleanupOptimisticResults(String cacheKey) { - if (cache is OptimisticCache) { - (cache as OptimisticCache).removeOptimisticPatch(cacheKey); - } + cache.removeOptimisticPatch(cacheKey); } /// Push changed data from cache to query streams diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index 21a0c1f1b..a57ac20cb 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -1,11 +1,14 @@ +import 'package:graphql/src/cache/cache.dart'; +import 'package:meta/meta.dart'; + import 'package:gql/ast.dart'; import 'package:gql/language.dart'; import 'package:gql_exec/gql_exec.dart'; + import 'package:graphql/client.dart'; import 'package:graphql/internal.dart'; import 'package:graphql/src/core/raw_operation_data.dart'; import 'package:graphql/src/utilities/helpers.dart'; -import 'package:meta/meta.dart'; /// Parse GraphQL query strings into the standard GraphQL AST. DocumentNode gql(String query) => parseString(query); @@ -100,6 +103,17 @@ class BaseOptions extends RawOperationData { /// Context to be passed to link execution chain. Context context; + + // TODO consider inverting this relationship + /// Resolve these options into a request + Request get asRequest => Request( + operation: Operation( + document: document, + operationName: operationName, + ), + variables: variables, + context: context ?? Context(), + ); } /// Query options. @@ -126,7 +140,10 @@ class QueryOptions extends BaseOptions { } typedef OnMutationCompleted = void Function(dynamic data); -typedef OnMutationUpdate = void Function(Cache cache, QueryResult result); +typedef OnMutationUpdate = void Function( + GraphQLDataProxy cache, + QueryResult result, +); typedef OnError = void Function(OperationException error); /// Mutation options @@ -154,7 +171,7 @@ class MutationOptions extends BaseOptions { class MutationCallbacks { final MutationOptions options; - final Cache cache; + final GraphQLCache cache; final String queryId; MutationCallbacks({ @@ -205,12 +222,13 @@ class MutationCallbacks { void _optimisticUpdate(QueryResult result) { final String patchId = _patchId; // this is also done in query_manager, but better safe than sorry - assert(cache is OptimisticCache, - "can't optimisticly update non-optimistic cache"); - (cache as OptimisticCache).addOptimisiticPatch(patchId, (Cache cache) { - options.update(cache, result); - return cache; - }); + cache.recordOptimisticTransaction( + (GraphQLDataProxy cache) { + options.update(cache, result); + return cache; + }, + patchId, + ); } // optimistic patches will be cleaned up by the query_manager diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index a2c8ac022..99c78d463 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -86,7 +86,7 @@ class GraphQLClient { final Link link; /// The initial [Cache] to use in the data store. - final Cache cache; + final GraphQLCache cache; QueryManager queryManager; diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index 64eae36b5..4931b318a 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -1,6 +1,3 @@ -import 'package:graphql/src/cache/lazy_cache_map.dart' - show LazyDereferencingMap, unwrapMap; - bool notNull(Object any) { return any != null; } @@ -36,8 +33,7 @@ Map _recursivelyAddAll( Map target, Map source, ) { - target = Map.from(unwrapMap(target)); - source = unwrapMap(source); + target = Map.from(target); source.forEach((String key, dynamic value) { if (target.containsKey(key) && target[key] is Map && @@ -65,8 +61,6 @@ Map _recursivelyAddAll( /// ])); /// // { keyA: a2, keyB: b3 } /// ``` -/// -/// All given [LazyDereferencingMap] instances will be unwrapped Map deeplyMergeLeft( Iterable> maps, ) { diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 042eb9c49..cbd19bef5 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: gql_http_link: ^0.2.9 gql_transform_link: ^0.1.5 quiver: ">=2.0.0 <3.0.0" + normalize: ^0.2.0 dev_dependencies: pedantic: ^1.8.0+1 mockito: ^4.0.0 From 61582b3d7bc15f3b1630175e39672d89db537ccf Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 13 May 2020 19:59:46 -0500 Subject: [PATCH 010/118] fix(examples): flutter bloc pubspec --- examples/flutter_bloc/pubspec.yaml | 2 +- v4_devnotes.md | 37 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 v4_devnotes.md diff --git a/examples/flutter_bloc/pubspec.yaml b/examples/flutter_bloc/pubspec.yaml index a32c0d414..0bed5fb67 100644 --- a/examples/flutter_bloc/pubspec.yaml +++ b/examples/flutter_bloc/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: graphql: path: ../../packages/graphql cupertino_icons: ^0.1.2 - flutter_bloc: ^3.2.0 + flutter_bloc: ^4.0.0 equatable: ^0.2.0 dev_dependencies: diff --git a/v4_devnotes.md b/v4_devnotes.md new file mode 100644 index 000000000..58a404905 --- /dev/null +++ b/v4_devnotes.md @@ -0,0 +1,37 @@ +# v4 Dev Notes + +## Differences between ferry_cache and graphql cache + +- The old cache was layered, ferry_cache is stream-based +- The ferry_cache api accepts optimistic as a parameter, whereas the old cache attached optimism info to the response data + +once you serialize the query request, +if you have to deserialize the cache update, you have no access to the callback + +update cache handlers are applied twice – optimistically then from network + +handling network failure more flexibility +possibly annotations + +optimism handled by proxy + +codegen difference + +lean on hive + +building on top of ferry client and cleint generator that would make the graphql api discovery easier +`client.queryName` +creating queries at build/runtime +limitation + +- fragments as a unit of composition + +single query controller which is a stream controller, +to make a query you add an event and the response stream picks it up + +since all queries are added to the same stream controller, +pagination works by taking multiple data events and running user defined update + +you can give mutations the same fetch policies + +queries, mutations and subscriptions all run through the same controller From f8699ee049a34ff730036b088c659f237d28e634 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 14 May 2020 10:53:49 -0500 Subject: [PATCH 011/118] refactor: ~ s/InMemoryCache/GraphQLCache/g --- packages/graphql/README.md | 13 +++++-------- packages/graphql/example/bin/main.dart | 2 +- .../graphql/test/in_memory_storage_io_test.dart | 2 +- packages/graphql/test/in_memory_storage_test.dart | 8 ++++---- packages/graphql_flutter/README.md | 4 ++-- packages/graphql_flutter/UPGRADE_GUIDE.md | 3 +-- .../graphql_flutter/example/lib/fetchmore/main.dart | 2 +- packages/graphql_flutter/lib/graphql_flutter.dart | 2 +- packages/graphql_flutter/lib/src/caches.dart | 4 ++-- .../graphql_flutter/test/widgets/query_test.dart | 2 +- 10 files changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/graphql/README.md b/packages/graphql/README.md index caf226cfc..5991e53f1 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -45,7 +45,7 @@ dev_dependencies: To connect to a GraphQL Server, we first need to create a `GraphQLClient`. A `GraphQLClient` requires both a `cache` and a `link` to be initialized. -In our example below, we will be using the Github Public API. In our example below, we are going to use `HttpLink` which we will concatenate with `AuthLink` so as to attach our github access token. For the cache, we are going to use `InMemoryCache`. +In our example below, we will be using the Github Public API. In our example below, we are going to use `HttpLink` which we will concatenate with `AuthLink` so as to attach our github access token. For the cache, we are going to use `GraphQLCache`. ```dart // ... @@ -61,7 +61,7 @@ final AuthLink _authLink = AuthLink( final Link _link = _authLink.concat(_httpLink); final GraphQLClient _client = GraphQLClient( - cache: InMemoryCache(), + cache: GraphQLCache(), link: _link, ); @@ -201,7 +201,7 @@ if (isStarred) { ### AST documents > We are deprecating `document` and recommend you update your application to use -`document` instead. `document` will be removed from the api in a future version. +> `document` instead. `document` will be removed from the api in a future version. For example: @@ -224,7 +224,7 @@ With [`package:gql_code_gen`](https://pub.dev/packages/gql_code_gen) you can par ```graphql mutation AddStar($starrableId: ID!) { - action: addStar(input: {starrableId: $starrableId}) { + action: addStar(input: { starrableId: $starrableId }) { starrable { viewerHasStarred } @@ -265,14 +265,12 @@ final ErrorLink errorLink = ErrorLink(errorHandler: (ErrorResponse response) { ### `PersistedQueriesLink` (experimental) -To improve performance you can make use of a concept introduced by [Apollo](https://www.apollographql.com/) called [Automatic persisted queries](https://www.apollographql.com/docs/apollo-server/performance/apq/) (or short "APQ") to send smaller requests and even enabled CDN caching for your GraphQL API. +To improve performance you can make use of a concept introduced by [Apollo](https://www.apollographql.com/) called [Automatic persisted queries](https://www.apollographql.com/docs/apollo-server/performance/apq/) (or short "APQ") to send smaller requests and even enabled CDN caching for your GraphQL API. **ATTENTION:** This also requires you to have a GraphQL server that supports APQ, like [Apollo's GraphQL Server](https://www.apollographql.com/docs/apollo-server/) and will only work for queries (but not for mutations or subscriptions). - You can than use it simply by prepending a `PersistedQueriesLink` to your normal `HttpLink`: - ```dart final PersistedQueriesLink _apqLink = PersistedQueriesLink( // To enable GET queries for the first load to allow for CDN caching @@ -286,7 +284,6 @@ final HttpLink _httpLink = HttpLink( final Link _link = _apqLink.concat(_httpLink); ``` - [build-status-badge]: https://img.shields.io/circleci/build/github/zino-app/graphql-flutter.svg?style=flat-square [build-status-link]: https://circleci.com/gh/zino-app/graphql-flutter [coverage-badge]: https://img.shields.io/codecov/c/github/zino-app/graphql-flutter.svg?style=flat-square diff --git a/packages/graphql/example/bin/main.dart b/packages/graphql/example/bin/main.dart index 4e994cf35..52e57e9fd 100644 --- a/packages/graphql/example/bin/main.dart +++ b/packages/graphql/example/bin/main.dart @@ -26,7 +26,7 @@ GraphQLClient client() { ); return GraphQLClient( - cache: InMemoryCache(), + cache: GraphQLCache(), link: _link, ); } diff --git a/packages/graphql/test/in_memory_storage_io_test.dart b/packages/graphql/test/in_memory_storage_io_test.dart index ec7f2f7aa..a4fb1598d 100644 --- a/packages/graphql/test/in_memory_storage_io_test.dart +++ b/packages/graphql/test/in_memory_storage_io_test.dart @@ -10,7 +10,7 @@ import 'helpers.dart'; void main() { group('In memory exception handling', () { test('FileSystemException', overridePrint((List log) async { - final InMemoryCache cache = InMemoryCache( + final GraphQLCache cache = GraphQLCache( storagePrefix: Future.error(FileSystemException()), ); await cache.restore(); diff --git a/packages/graphql/test/in_memory_storage_test.dart b/packages/graphql/test/in_memory_storage_test.dart index 5db761e11..e6c29c563 100644 --- a/packages/graphql/test/in_memory_storage_test.dart +++ b/packages/graphql/test/in_memory_storage_test.dart @@ -41,7 +41,7 @@ final Map eData = { void main() { group('Normalizes writes', () { test('.write .read round trip', () async { - final InMemoryCache cache = InMemoryCache(); + final GraphQLCache cache = GraphQLCache(); cache.write(aKey, aData); await cache.save(); cache.reset(); @@ -51,7 +51,7 @@ void main() { test('.write avoids overriding a superset with a subset of a field (#155)', () async { - final InMemoryCache cache = InMemoryCache(); + final GraphQLCache cache = GraphQLCache(); cache.write(aKey, aData); final Map anotherAData = { @@ -73,7 +73,7 @@ void main() { }); test('.write does not mutate input', () async { - final InMemoryCache cache = InMemoryCache(); + final GraphQLCache cache = GraphQLCache(); cache.write(aKey, aData); final Map anotherAData = { 'a': { @@ -97,7 +97,7 @@ void main() { }); test('saving concurrently wont error', () async { - final InMemoryCache cache = InMemoryCache(); + final GraphQLCache cache = GraphQLCache(); cache.write(aKey, aData); cache.write(bKey, bData); cache.write(cKey, cData); diff --git a/packages/graphql_flutter/README.md b/packages/graphql_flutter/README.md index 72aa21652..c6811f72f 100644 --- a/packages/graphql_flutter/README.md +++ b/packages/graphql_flutter/README.md @@ -51,7 +51,7 @@ Find the migration from version 2 to version 3 [here](./../../changelog-v2-v3.md ## Usage -To use the client it first needs to be initialized with a link and cache. For this example, we will be using an `HttpLink` as our link and `InMemoryCache` as our cache. If your endpoint requires authentication you can concatenate the `AuthLink`, it resolves the credentials using a future, so you can authenticate asynchronously. +To use the client it first needs to be initialized with a link and cache. For this example, we will be using an `HttpLink` as our link and `GraphQLCache` as our cache. If your endpoint requires authentication you can concatenate the `AuthLink`, it resolves the credentials using a future, so you can authenticate asynchronously. > For this example we will use the public GitHub API. @@ -75,7 +75,7 @@ void main() { ValueNotifier client = ValueNotifier( GraphQLClient( - cache: InMemoryCache(), + cache: GraphQLCache(), link: link, ), ); diff --git a/packages/graphql_flutter/UPGRADE_GUIDE.md b/packages/graphql_flutter/UPGRADE_GUIDE.md index 0c4d8d367..6528632e9 100644 --- a/packages/graphql_flutter/UPGRADE_GUIDE.md +++ b/packages/graphql_flutter/UPGRADE_GUIDE.md @@ -25,7 +25,7 @@ void main() { - Client( - endPoint: 'https://api.github.com/graphql', + GraphQLClient( - cache: InMemoryCache(), + cache: GraphQLCache(), - apiToken: '', + link: link, ), @@ -103,4 +103,3 @@ Mutation( ``` That's it! You should now be able to use the latest version of our library. - diff --git a/packages/graphql_flutter/example/lib/fetchmore/main.dart b/packages/graphql_flutter/example/lib/fetchmore/main.dart index 082f15462..ded8b6b5f 100644 --- a/packages/graphql_flutter/example/lib/fetchmore/main.dart +++ b/packages/graphql_flutter/example/lib/fetchmore/main.dart @@ -22,7 +22,7 @@ class FetchMoreWidgetScreen extends StatelessWidget { final client = ValueNotifier( GraphQLClient( - cache: InMemoryCache(), + cache: GraphQLCache(), link: link, ), ); diff --git a/packages/graphql_flutter/lib/graphql_flutter.dart b/packages/graphql_flutter/lib/graphql_flutter.dart index f49c309ea..537e0a0ce 100644 --- a/packages/graphql_flutter/lib/graphql_flutter.dart +++ b/packages/graphql_flutter/lib/graphql_flutter.dart @@ -1,7 +1,7 @@ library graphql_flutter; export 'package:graphql/client.dart' - hide InMemoryCache, NormalizedInMemoryCache, OptimisticCache; + hide GraphQLCache, NormalizedInMemoryCache, OptimisticCache; export 'package:graphql_flutter/src/caches.dart'; diff --git a/packages/graphql_flutter/lib/src/caches.dart b/packages/graphql_flutter/lib/src/caches.dart index b38988456..ae1efe629 100644 --- a/packages/graphql_flutter/lib/src/caches.dart +++ b/packages/graphql_flutter/lib/src/caches.dart @@ -9,8 +9,8 @@ import 'package:graphql/client.dart' as client; final FutureOr flutterStoragePrefix = (() async => (await getApplicationDocumentsDirectory()).path)(); -class InMemoryCache extends client.InMemoryCache { - InMemoryCache({ +class GraphQLCache extends client.GraphQLCache { + GraphQLCache({ FutureOr storagePrefix, }) : super(storagePrefix: storagePrefix ?? flutterStoragePrefix); } diff --git a/packages/graphql_flutter/test/widgets/query_test.dart b/packages/graphql_flutter/test/widgets/query_test.dart index 0b5634469..0545d8aee 100644 --- a/packages/graphql_flutter/test/widgets/query_test.dart +++ b/packages/graphql_flutter/test/widgets/query_test.dart @@ -90,7 +90,7 @@ void main() { ); client = ValueNotifier( GraphQLClient( - cache: InMemoryCache(storagePrefix: 'test'), + cache: GraphQLCache(storagePrefix: 'test'), link: httpLink, ), ); From 283326f32213a6c7747fb4ac9caaca157f04f1d8 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 14 May 2020 10:54:02 -0500 Subject: [PATCH 012/118] docs: store api --- packages/graphql/lib/src/cache/store.dart | 26 ++++++++++++++++--- .../graphql/test/in_memory_storage_test.dart | 2 +- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/graphql/lib/src/cache/store.dart b/packages/graphql/lib/src/cache/store.dart index 2f8af94b8..c10f3704c 100644 --- a/packages/graphql/lib/src/cache/store.dart +++ b/packages/graphql/lib/src/cache/store.dart @@ -2,28 +2,47 @@ import 'dart:collection'; import 'package:meta/meta.dart'; +// TODO decide if [Store] should have save, etc +// TODO figure out how to reference non-imported symbols +/// Raw key-value datastore API leveraged by the [Cache] @immutable abstract class Store { Map get(String dataId); + /// Write [value] into this store under the key [dataId] void put(String dataId, Map value); + /// [put] all entries from [data] into the store + /// + /// Functionally equivalent to `data.map(put);` void putAll(Map> data); + /// Delete the value of the [dataId] from the store, if preset void delete(String dataId); + /// Empty the store void reset(); + /// Return the entire contents of the cache as [Map]. + /// + /// NOTE: some [Store]s might return mutable objects + /// referenced by the store itself. Map> toMap(); } +/// Simplest possible [Map]-backed store @immutable class InMemoryStore extends Store { + /// Normalized map that backs the store. + /// Defaults to an empty [HashMap] @protected final Map data; - InMemoryStore([Map data]) - : data = data ?? HashMap(); + /// Creates an InMemoryStore inititalized with [data], + /// which defaults to an empty [HashMap] + InMemoryStore([ + Map data, + ]) : data = data ?? HashMap(); @override Map get(String dataId) => data[dataId]; @@ -38,8 +57,9 @@ class InMemoryStore extends Store { @override void delete(String dataId) => data.remove(dataId); + /// Return the underlying [data] as an unmodifiable [Map]. @override - Map> toMap() => data; + Map> toMap() => Map.unmodifiable(data); void reset() => data.clear(); } diff --git a/packages/graphql/test/in_memory_storage_test.dart b/packages/graphql/test/in_memory_storage_test.dart index e6c29c563..7e479f955 100644 --- a/packages/graphql/test/in_memory_storage_test.dart +++ b/packages/graphql/test/in_memory_storage_test.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'package:test/test.dart'; -import 'package:graphql/src/cache/in_memory.dart'; +import 'package:graphql/src/cache/cache.dart'; const String aKey = 'aKey'; const String bKey = 'bKey'; From c8000bbec7678547ceab574800e8f86fda2d5e7f Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 14 May 2020 12:57:27 -0500 Subject: [PATCH 013/118] docs, tests: new cache normalization first pass --- .../src/cache/_optimistic_transactions.dart | 50 ++++ packages/graphql/lib/src/cache/cache.dart | 69 ++--- .../lib/src/cache/normalizing_data_proxy.dart | 32 ++- .../graphql/lib/src/core/query_manager.dart | 7 +- .../graphql/lib/src/utilities/helpers.dart | 2 + .../graphql/test/cache/normalization.dart | 52 ++++ .../test/cache/normalization_data.dart | 228 ++++++++++++++++ .../optimism.dart} | 3 + packages/graphql/test/helpers.dart | 4 +- .../test/in_memory_storage_io_test.dart | 23 -- .../graphql/test/in_memory_storage_test.dart | 125 --------- .../test/normalized_in_memory_test.dart | 244 ------------------ 12 files changed, 388 insertions(+), 451 deletions(-) create mode 100644 packages/graphql/lib/src/cache/_optimistic_transactions.dart create mode 100644 packages/graphql/test/cache/normalization.dart create mode 100644 packages/graphql/test/cache/normalization_data.dart rename packages/graphql/test/{optimistic_cache_test.dart => cache/optimism.dart} (99%) delete mode 100644 packages/graphql/test/in_memory_storage_io_test.dart delete mode 100644 packages/graphql/test/in_memory_storage_test.dart delete mode 100644 packages/graphql/test/normalized_in_memory_test.dart diff --git a/packages/graphql/lib/src/cache/_optimistic_transactions.dart b/packages/graphql/lib/src/cache/_optimistic_transactions.dart new file mode 100644 index 000000000..3457edcd7 --- /dev/null +++ b/packages/graphql/lib/src/cache/_optimistic_transactions.dart @@ -0,0 +1,50 @@ +/// Optimistic proxying and patching classes and typedefs used by `./cache.dart` +import 'dart:collection'; + +import 'package:meta/meta.dart'; + +import 'package:graphql/src/cache/normalizing_data_proxy.dart'; +import 'package:graphql/src/cache/data_proxy.dart'; + +import 'package:graphql/src/cache/cache.dart' show GraphQLCache; + +/// API for users to provide cache updates through +typedef CacheTransaction = GraphQLDataProxy Function(GraphQLDataProxy proxy); + +/// An optimistic update recorded with [GraphQLCache.recordOptimisticTransaction], +/// identifiable through it's [id]. +@immutable +class OptimisticPatch extends Object { + const OptimisticPatch(this.id, this.data); + final String id; + final HashMap data; +} + +/// Proxy by which users record [_OptimisticPatch]s though +/// [GraphQLCache.recordOptimisticTransaction]. +/// +/// Implements, and is exposed as, a [GraphQLDataProxy]. +/// It's `optimistic` paraemeters default to `true`, +/// but the user can override them to read directly from the `store`. +class OptimisticProxy extends NormalizingDataProxy { + OptimisticProxy(this.cache); + + GraphQLCache cache; + + HashMap data = HashMap(); + + @override + dynamic readNormalized(String rootId, {bool optimistic = true}) { + if (!optimistic) { + return cache.readNormalized(rootId, optimistic: false); + } + // the cache calls `patch.data.containsKey(rootId)`, + // so this is not an infinite loop + return data[rootId] ?? cache.readNormalized(rootId, optimistic: true); + } + + @override + void writeNormalized(String dataId, dynamic value) => data[dataId] = value; + + OptimisticPatch asPatch(String id) => OptimisticPatch(id, data); +} diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index bc1220e62..b7e62d2f0 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -1,43 +1,13 @@ -import 'dart:collection'; - import 'package:graphql/src/cache/normalizing_data_proxy.dart'; import 'package:meta/meta.dart'; -import 'package:graphql/src/cache/data_proxy.dart'; - import 'package:graphql/src/utilities/helpers.dart'; import 'package:graphql/src/cache/store.dart'; -export './data_proxy.dart'; - -typedef CacheTransaction = GraphQLDataProxy Function(GraphQLDataProxy proxy); - -class OptimisticPatch extends Object { - OptimisticPatch(this.id, this.data); - String id; - HashMap data; -} - -class OptimisticProxy extends NormalizingDataProxy { - OptimisticProxy(this.cache); - - GraphQLCache cache; - - HashMap data = HashMap(); +import 'package:graphql/src/cache/_optimistic_transactions.dart'; - @override - dynamic read(String rootId, {bool optimistic = true}) { - if (!optimistic) { - return cache.read(rootId, optimistic: false); - } - // the cache calls `patch.data.containsKey(rootId)`, - // so this is not an infinite loop - return data[rootId] ?? cache.read(rootId, optimistic: true); - } - - @override - void write(String dataId, dynamic value) => data[dataId] = value; -} +export 'package:graphql/src/cache/store.dart'; +export 'package:graphql/src/cache/data_proxy.dart'; class GraphQLCache extends NormalizingDataProxy { GraphQLCache({ @@ -45,24 +15,30 @@ class GraphQLCache extends NormalizingDataProxy { this.dataIdFromObject, }) : store = store ?? InMemoryStore(); + /// Stores the underlying normalized data @protected final Store store; final DataIdResolver dataIdFromObject; + /// List of patches recorded through [recordOptimisticTransaction] + /// + /// They are applied in ascending order, + /// thus data in `last` will overwrite that in `first` + /// if there is a conflict @protected - List optimisticPatches = []; + List optimisticPatches = []; - /// Reads and dereferences an entity from the first valid optimistic layer, + /// Reads dereferences an entity from the first valid optimistic layer, /// defaulting to the base internal HashMap. - Object read(String rootId, {bool optimistic = true}) { + Object readNormalized(String rootId, {bool optimistic = true}) { Object value = store.get(rootId); if (!optimistic) { return value; } - for (OptimisticPatch patch in optimisticPatches) { + for (final patch in optimisticPatches) { if (patch.data.containsKey(rootId)) { final Object patchData = patch.data[rootId]; if (value is Map && patchData is Map) { @@ -79,9 +55,8 @@ class GraphQLCache extends NormalizingDataProxy { return value; } - void write(String dataId, dynamic value) => store.put(dataId, value); - - OptimisticProxy get _proxy => OptimisticProxy(this); + void writeNormalized(String dataId, dynamic value) => + store.put(dataId, value); String _parentPatchId(String id) { final List parts = id.split('.'); @@ -93,7 +68,7 @@ class GraphQLCache extends NormalizingDataProxy { bool _patchExistsFor(String id) => optimisticPatches.firstWhere( - (OptimisticPatch patch) => patch.id == id, + (patch) => patch.id == id, orElse: () => null, ) != null; @@ -107,7 +82,8 @@ class GraphQLCache extends NormalizingDataProxy { return parentId == null || _patchExistsFor(parentId); } - /// Add a given patch using the given [transform] + // TODO does patch hierachy still makes sense + /// Record the given [transaction] into a patch with the id [addId] /// /// 1 level of hierarchical optimism is supported: /// * if a patch has the id `$queryId.child`, it will be removed with `$queryId` @@ -120,21 +96,22 @@ class GraphQLCache extends NormalizingDataProxy { CacheTransaction transaction, String addId, ) { - final OptimisticProxy patch = transaction(_proxy) as OptimisticProxy; + final _proxy = transaction(OptimisticProxy(this)) as OptimisticProxy; if (_safeToAdd(addId)) { - optimisticPatches.add(OptimisticPatch(addId, patch.data)); + optimisticPatches.add(_proxy.asPatch(addId)); } } /// Remove a given patch from the list /// /// This will also remove all "nested" patches, such as `$queryId.update` + /// (see [recordOptimisticTransaction]) + /// /// This allows for hierarchical optimism that is automatically cleaned up /// without having to tightly couple optimistic changes void removeOptimisticPatch(String removeId) { optimisticPatches.removeWhere( - (OptimisticPatch patch) => - patch.id == removeId || _parentPatchId(patch.id) == removeId, + (patch) => patch.id == removeId || _parentPatchId(patch.id) == removeId, ); } } diff --git a/packages/graphql/lib/src/cache/normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/normalizing_data_proxy.dart index 74fefa126..ee20ac74d 100644 --- a/packages/graphql/lib/src/cache/normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/normalizing_data_proxy.dart @@ -9,25 +9,40 @@ import './data_proxy.dart'; typedef DataIdResolver = String Function(Map object); -/// Implements the core normalization api leveraged by the cache and proxy +/// Implements the core (de)normalization api leveraged by the cache and proxy, /// -/// `read` and `write` must still be supplied by the implementing class +/// [readNormalized] and [writeNormalized] must still be supplied by the implementing class abstract class NormalizingDataProxy extends GraphQLDataProxy { + /// `typePolicies` to pass down to `normalize` Map typePolicies; + + /// Whether to add `__typenames` automatically bool addTypename; + /// Optional `dataIdFromObject` function to pass through to [normalize] DataIdResolver dataIdFromObject; - dynamic read(String rootId, {bool optimistic}); + /// Read normaized data from the cache + /// + /// Called from [readQuery] and [readFragment], which handle denormalization. + /// + /// The key differentiating factor for an implementing `cache` or `proxy` + /// is usually how they handle [optimistic] reads. + @protected + dynamic readNormalized(String rootId, {bool optimistic}); - void write(String dataId, dynamic value); + /// Write normalized data into the cache. + /// + /// Called from [writeQuery] and [writeFragment] + @protected + void writeNormalized(String dataId, dynamic value); Map readQuery( Request request, { bool optimistic = true, }) => denormalize( - reader: (dataId) => read(dataId, optimistic: optimistic), + reader: (dataId) => readNormalized(dataId, optimistic: optimistic), query: request.operation.document, operationName: request.operation.operationName, variables: request.variables, @@ -43,7 +58,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { bool optimistic = true, }) => denormalizeFragment( - reader: (dataId) => read(dataId, optimistic: optimistic), + reader: (dataId) => readNormalized(dataId, optimistic: optimistic), fragment: fragment, idFields: idFields, fragmentName: fragmentName, @@ -53,14 +68,13 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { dataIdFromObject: dataIdFromObject, ); - /// [normalize] the given `data` into the cache using graphql metadata from `request` void writeQuery( Request request, Map data, { String queryId, }) => normalize( - writer: (dataId, value) => write(dataId, value), + writer: (dataId, value) => writeNormalized(dataId, value), query: request.operation.document, operationName: request.operation.operationName, variables: request.variables, @@ -78,7 +92,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { String queryId, }) => normalizeFragment( - writer: (dataId, value) => write(dataId, value), + writer: (dataId, value) => writeNormalized(dataId, value), fragment: fragment, idFields: idFields, data: data, diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 584c569c0..046f0c920 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -287,15 +287,20 @@ class QueryManager { /// /// rebroadcast queries inherit `optimistic` /// from the triggering state-change + // TODO ^ no longer true. I would like to recoup the entity-wise + // TODO cache state optimistic awareness void rebroadcastQueries() { for (ObservableQuery query in queries.values) { if (query.isRebroadcastSafe) { - final dynamic cachedData = cache.read(query.options.toKey()); + // TODO use queryId everywhere or nah + final dynamic cachedData = + cache.readQuery(query.options.asRequest, optimistic: true); if (cachedData != null) { query.addResult( mapFetchResultToQueryResult( Response(data: cachedData), query.options, + // TODO maybe entirely wrong source: QueryResultSource.Cache, ), ); diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index 4931b318a..139f7ae51 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -61,6 +61,8 @@ Map _recursivelyAddAll( /// ])); /// // { keyA: a2, keyB: b3 } /// ``` +/// +/// Conflicting [List]s are overwritten like scalars Map deeplyMergeLeft( Iterable> maps, ) { diff --git a/packages/graphql/test/cache/normalization.dart b/packages/graphql/test/cache/normalization.dart new file mode 100644 index 000000000..3a0b51320 --- /dev/null +++ b/packages/graphql/test/cache/normalization.dart @@ -0,0 +1,52 @@ +import 'package:test/test.dart'; + +import 'package:graphql/src/cache/cache.dart'; + +import '../helpers.dart'; +import 'normalization_data.dart'; + +void main() { + group('Normalizes writes', () { + final GraphQLCache cache = getTestCache(); + test('.writeQuery .readQuery round trip', () { + cache.writeQuery(basicTest.request, basicTest.data); + expect(cache.readQuery(basicTest.request), equals(basicTest.data)); + }); + test('updating nested normalized data changes top level operation', () { + cache.writeNormalized('C:6', updatedCValue); + expect( + cache.readQuery(basicTest.request), + equals(updatedCBasicTestData), + ); + }); + test('updating subset query only partially overrides superset query', () { + cache.writeQuery( + basicTestSubsetAValue.request, basicTestSubsetAValue.data); + expect(cache.readQuery(basicTest.request), + equals(updatedSubsetOperationData)); + }); + }); + + group('Handles cyclical references', () { + final GraphQLCache cache = getTestCache(); + test('lazily reads cyclical references', () { + cache.writeQuery(cyclicalTest.request, cyclicalTest.data); + for (final normalized in cyclicalTest.normalizedEntities) { + final dataId = "${normalized['__typename']}:${normalized['id']}"; + expect(cache.readNormalized(dataId), equals(normalized)); + } + }); + }); + + group('Handles Object/pointer self-references/cycles', () { + final GraphQLCache cache = getTestCache(); + test('correctly reads cyclical references', () { + cyclicalTest.data = cyclicalObjOperationData; + cache.writeQuery(cyclicalTest.request, cyclicalTest.data); + for (final normalized in cyclicalTest.normalizedEntities) { + final dataId = "${normalized['__typename']}:${normalized['id']}"; + expect(cache.readNormalized(dataId), equals(normalized)); + } + }); + }); +} diff --git a/packages/graphql/test/cache/normalization_data.dart b/packages/graphql/test/cache/normalization_data.dart new file mode 100644 index 000000000..212c2551d --- /dev/null +++ b/packages/graphql/test/cache/normalization_data.dart @@ -0,0 +1,228 @@ +import 'package:gql_exec/gql_exec.dart'; +import 'package:gql/language.dart'; +import 'package:graphql/internal.dart'; +import 'package:meta/meta.dart'; + +const String rawOperationKey = 'rawOperationKey'; + +class TestCase { + TestCase( + String operationName, { + @required this.data, + @required String operation, + Map variables = const {}, + this.normalizedEntities, + }) : request = Request( + operation: Operation( + document: parseString(operation), + operationName: operationName, + ), + variables: variables, + context: Context(), + ); + + Request request; + + /// data to write to cache + Map data; + + /// entities to inspect the store for, if any + List> normalizedEntities; +} + +final basicTest = TestCase( + 'basicTestOperation', + operation: r'''{ + a { + __typename + id + # union + list { + __typename + value + ... on Item { id } + } + b { + __typename + id + c { + __typename + id, + cField + } + bField { field } + }, + d { + id, + dField {field} + } + aField { field } + } + }''', + data: { + 'a': { + '__typename': 'A', + 'id': 1, + 'list': [ + {'__typename': 'Num', 'value': 1}, + {'__typename': 'Num', 'value': 2}, + {'__typename': 'Num', 'value': 3}, + {'__typename': 'Item', 'id': 4, 'value': 4} + ], + 'b': { + '__typename': 'B', + 'id': 5, + 'c': { + '__typename': 'C', + 'id': 6, + 'cField': 'value', + }, + 'bField': {'field': true} + }, + 'd': { + 'id': 9, + 'dField': {'field': true} + }, + }, + 'aField': {'field': false} + }, +); + +final Map updatedCValue = { + '__typename': 'C', + 'id': 6, + 'new': 'field', + 'cField': 'changed value', +}; + +final Map updatedCBasicTestData = deeplyMergeLeft([ + basicTest.data, + { + 'a': { + 'b': { + 'c': { + '__typename': 'C', + 'id': 6, + 'cField': 'changed value', + }, + }, + }, + }, +]); + +final basicTestSubsetAValue = TestCase( + 'basicTestSubsetAValue', + operation: r'''{ + a { + __typename + id + list { + __typename + value + ... on Item { id } + } + d { id } + } + }''', + data: { + 'a': { + '__typename': 'A', + 'id': 1, + 'list': [ + {'__typename': 'Num', 'value': 5}, + {'__typename': 'Num', 'value': 6}, + {'__typename': 'Num', 'value': 7}, + { + '__typename': 'Item', + 'id': 8, + 'value': 8, + } + ], + 'd': { + 'id': 10, + }, + }, + }, +); + +final Map updatedSubsetOperationData = { + 'a': { + '__typename': 'A', + 'id': 1, + 'list': basicTest.data['a']['list'], + 'b': { + '__typename': 'B', + 'id': 5, + 'c': { + '__typename': 'C', + 'id': 6, + 'cField': 'changed value', + }, + 'bField': {'field': true} + }, + 'd': { + 'id': 10, + 'dField': {'field': true} + }, + }, + 'aField': {'field': false} +}; + +final cyclicalTest = TestCase('cyclicalTestOperation', operation: r'''{ + a { + __typename + id + b { + __typename + id + as { + __typename + id + } + } + } + }''', data: { + 'a': { + '__typename': 'A', + 'id': 1, + 'b': { + '__typename': 'B', + 'id': 5, + 'as': [ + { + '__typename': 'A', + 'id': 1, + }, + ] + }, + }, +}, normalizedEntities: [ + { + '__typename': 'A', + 'id': 1, + 'b': {r"$ref": 'B:5'} + }, + { + '__typename': 'B', + 'id': 5, + 'as': [ + {r"$ref": 'A:1'} + ], + }, +]); + +Map get cyclicalObjOperationData { + Map a; + Map b; + a = { + '__typename': 'A', + 'id': 1, + }; + b = { + '__typename': 'B', + 'id': 5, + 'as': [a] + }; + a['b'] = b; + return {'a': a}; +} diff --git a/packages/graphql/test/optimistic_cache_test.dart b/packages/graphql/test/cache/optimism.dart similarity index 99% rename from packages/graphql/test/optimistic_cache_test.dart rename to packages/graphql/test/cache/optimism.dart index 2f7e35c34..de6eebcb4 100644 --- a/packages/graphql/test/optimistic_cache_test.dart +++ b/packages/graphql/test/cache/optimism.dart @@ -1,3 +1,4 @@ +/* import 'package:test/test.dart'; import 'package:graphql/src/cache/normalized_in_memory.dart' @@ -210,3 +211,5 @@ void main() { }); }); } + +*/ diff --git a/packages/graphql/test/helpers.dart b/packages/graphql/test/helpers.dart index 04aeca0cb..a9f37d7ca 100644 --- a/packages/graphql/test/helpers.dart +++ b/packages/graphql/test/helpers.dart @@ -10,6 +10,4 @@ overridePrint(testFn(List log)) => () { return Zone.current.fork(specification: spec).run(() => testFn(log)); }; -NormalizedInMemoryCache getTestCache() => NormalizedInMemoryCache( - dataIdFromObject: typenameDataIdFromObject, - ); +GraphQLCache getTestCache() => GraphQLCache(); diff --git a/packages/graphql/test/in_memory_storage_io_test.dart b/packages/graphql/test/in_memory_storage_io_test.dart deleted file mode 100644 index a4fb1598d..000000000 --- a/packages/graphql/test/in_memory_storage_io_test.dart +++ /dev/null @@ -1,23 +0,0 @@ -@TestOn("vm") - -import 'dart:async'; -import 'dart:io'; -import 'package:test/test.dart'; -import 'package:graphql/src/cache/in_memory_io.dart'; - -import 'helpers.dart'; - -void main() { - group('In memory exception handling', () { - test('FileSystemException', overridePrint((List log) async { - final GraphQLCache cache = GraphQLCache( - storagePrefix: Future.error(FileSystemException()), - ); - await cache.restore(); - expect( - log, - ['Can\'t read file from storage, returning an empty HashMap.'], - ); - })); - }); -} diff --git a/packages/graphql/test/in_memory_storage_test.dart b/packages/graphql/test/in_memory_storage_test.dart deleted file mode 100644 index 7e479f955..000000000 --- a/packages/graphql/test/in_memory_storage_test.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'dart:async'; -import 'package:test/test.dart'; -import 'package:graphql/src/cache/cache.dart'; - -const String aKey = 'aKey'; -const String bKey = 'bKey'; -const String cKey = 'cKey'; -const String dKey = 'dKey'; -const String eKey = 'eKey'; - -final Map aData = { - 'a': { - '__typename': 'A', - } -}; - -final Map bData = { - 'b': { - '__typename': 'B', - } -}; - -final Map cData = { - 'c': { - '__typename': 'C', - } -}; - -final Map dData = { - 'd': { - '__typename': 'D', - } -}; - -final Map eData = { - 'e': { - '__typename': 'E', - } -}; - -void main() { - group('Normalizes writes', () { - test('.write .read round trip', () async { - final GraphQLCache cache = GraphQLCache(); - cache.write(aKey, aData); - await cache.save(); - cache.reset(); - await cache.restore(); - expect(cache.read(aKey), equals(aData)); - }); - - test('.write avoids overriding a superset with a subset of a field (#155)', - () async { - final GraphQLCache cache = GraphQLCache(); - cache.write(aKey, aData); - - final Map anotherAData = { - 'a': { - 'key': 'val', - }, - }; - cache.write(aKey, anotherAData); - - await cache.save(); - cache.reset(); - await cache.restore(); - expect( - cache.read(aKey), - equals({ - 'a': {'__typename': 'A', 'key': 'val'} - }), - ); - }); - - test('.write does not mutate input', () async { - final GraphQLCache cache = GraphQLCache(); - cache.write(aKey, aData); - final Map anotherAData = { - 'a': { - 'key': 'val', - }, - }; - cache.write(aKey, anotherAData); - - expect( - aData, - equals({ - 'a': {'__typename': 'A'} - }), - ); - expect( - anotherAData, - equals({ - 'a': {'key': 'val'} - }), - ); - }); - - test('saving concurrently wont error', () async { - final GraphQLCache cache = GraphQLCache(); - cache.write(aKey, aData); - cache.write(bKey, bData); - cache.write(cKey, cData); - cache.write(dKey, dData); - cache.write(eKey, eData); - - await Future.wait(>[ - cache.save(), - cache.save(), - cache.save(), - cache.save(), - cache.save(), - ]); - - cache.reset(); - await cache.restore(); - - expect(cache.read(aKey), equals(aData)); - expect(cache.read(bKey), equals(bData)); - expect(cache.read(cKey), equals(cData)); - expect(cache.read(dKey), equals(dData)); - expect(cache.read(eKey), equals(eData)); - }); - }); -} diff --git a/packages/graphql/test/normalized_in_memory_test.dart b/packages/graphql/test/normalized_in_memory_test.dart deleted file mode 100644 index 853c48f8a..000000000 --- a/packages/graphql/test/normalized_in_memory_test.dart +++ /dev/null @@ -1,244 +0,0 @@ -import 'package:test/test.dart'; - -import 'package:graphql/src/cache/normalized_in_memory.dart'; -import 'package:graphql/src/cache/lazy_cache_map.dart'; - -List reference(String key) { - return ['cache/reference', key]; -} - -const String rawOperationKey = 'rawOperationKey'; - -final Map rawOperationData = { - 'a': { - '__typename': 'A', - 'id': 1, - 'list': [ - 1, - 2, - 3, - { - '__typename': 'Item', - 'id': 4, - 'value': 4, - } - ], - 'b': { - '__typename': 'B', - 'id': 5, - 'c': { - '__typename': 'C', - 'id': 6, - 'cField': 'value', - }, - 'bField': {'field': true} - }, - 'd': { - 'id': 9, - 'dField': {'field': true} - }, - }, - 'aField': {'field': false} -}; - -final Map updatedCValue = { - '__typename': 'C', - 'id': 6, - 'new': 'field', - 'cField': 'changed value', -}; - -final Map updatedCOperationData = { - 'a': { - '__typename': 'A', - 'id': 1, - 'list': [ - 1, - 2, - 3, - { - '__typename': 'Item', - 'id': 4, - 'value': 4, - } - ], - 'b': { - '__typename': 'B', - 'id': 5, - 'c': updatedCValue, - 'bField': {'field': true} - }, - 'd': { - 'id': 9, - 'dField': {'field': true} - }, - }, - 'aField': {'field': false} -}; - -final Map subsetAValue = { - 'a': { - '__typename': 'A', - 'id': 1, - 'list': [ - 5, - 6, - 7, - { - '__typename': 'Item', - 'id': 8, - 'value': 8, - } - ], - 'd': { - 'id': 10, - }, - }, -}; - -final Map updatedSubsetOperationData = { - 'a': { - '__typename': 'A', - 'id': 1, - 'list': [ - 5, - 6, - 7, - { - '__typename': 'Item', - 'id': 8, - 'value': 8, - } - ], - 'b': { - '__typename': 'B', - 'id': 5, - 'c': updatedCValue, - 'bField': {'field': true} - }, - 'd': { - 'id': 10, - 'dField': {'field': true} - }, - }, - 'aField': {'field': false} -}; - -final Map cyclicalOperationData = { - 'a': { - '__typename': 'A', - 'id': 1, - 'b': { - '__typename': 'B', - 'id': 5, - 'as': [ - { - '__typename': 'A', - 'id': 1, - }, - ] - }, - }, -}; - -final Map cyclicalNormalizedA = { - '__typename': 'A', - 'id': 1, - 'b': ['@cache/reference', 'B/5'], -}; - -final Map cyclicalNormalizedB = { - '__typename': 'B', - 'id': 5, - 'as': [ - ['@cache/reference', 'A/1'] - ], -}; - -Map get cyclicalObjOperationData { - Map a; - Map b; - a = { - '__typename': 'A', - 'id': 1, - }; - b = { - '__typename': 'B', - 'id': 5, - 'as': [a] - }; - a['b'] = b; - return {'a': a}; -} - -final Map cyclicalObjNormalizedA = { - '__typename': 'A', - 'id': 1, - 'b': ['@cache/reference', 'B/5'], -}; - -final Map cyclicalObjNormalizedB = { - '__typename': 'B', - 'id': 5, - 'as': [ - ['@cache/reference', 'A/1'] - ], -}; - -NormalizedInMemoryCache getTestCache() => NormalizedInMemoryCache( - dataIdFromObject: typenameDataIdFromObject, - ); - -void main() { - group('Normalizes writes', () { - final NormalizedInMemoryCache cache = getTestCache(); - test('.write .readDenormalize round trip', () { - cache.write(rawOperationKey, rawOperationData); - expect(cache.denormalizedRead(rawOperationKey), equals(rawOperationData)); - }); - test('updating nested data changes top level operation', () { - cache.write('C/6', updatedCValue); - expect( - cache.denormalizedRead(rawOperationKey), - equals(updatedCOperationData), - ); - }); - test('updating subset query does not override superset query', () { - cache.write('anotherUnrelatedKey', subsetAValue); - expect(cache.read(rawOperationKey), equals(updatedSubsetOperationData)); - }); - }); - group('Normalizes writes', () { - final NormalizedInMemoryCache cache = getTestCache(); - test('lazily reads cyclical references', () { - cache.write(rawOperationKey, cyclicalOperationData); - final LazyCacheMap a = cache.read('A/1') as LazyCacheMap; - expect(a.data, equals(cyclicalNormalizedA)); - final LazyCacheMap b = a['b'] as LazyCacheMap; - expect(b.data, equals(cyclicalNormalizedB)); - }); - }); - - group('Handles Object/pointer self-references/cycles', () { - final NormalizedInMemoryCache cache = getTestCache(); - test('lazily reads cyclical references', () { - cache.write(rawOperationKey, cyclicalObjOperationData); - final LazyCacheMap a = cache.read('A/1') as LazyCacheMap; - expect(a.data, equals(cyclicalObjNormalizedA)); - final LazyCacheMap b = a['b'] as LazyCacheMap; - expect(b.data, equals(cyclicalObjNormalizedB)); - }); - }); - - group('Resets cache correctly', () { - final NormalizedInMemoryCache cache = getTestCache(); - test('resets cache correctly when cache.reset() is called', () { - cache.write(rawOperationKey, cyclicalObjOperationData); - final LazyCacheMap a = cache.read('A/1') as LazyCacheMap; - expect(a.data, equals(cyclicalObjNormalizedA)); - cache.reset(); - final resetA = cache.read('A/1'); - expect(resetA, equals(null)); - }); - }); -} From 27762016b3f108808bf66a0e32d2609bb9f9bd78 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 14 May 2020 14:15:14 -0500 Subject: [PATCH 014/118] normalization tests pass --- .../src/cache/_optimistic_transactions.dart | 17 ++++++++++-- packages/graphql/lib/src/cache/cache.dart | 22 +++++++++++++++- .../lib/src/cache/normalizing_data_proxy.dart | 10 ++++--- .../graphql/lib/src/utilities/helpers.dart | 1 + .../test/cache/normalization_data.dart | 26 +++++++------------ ...alization.dart => normalization_test.dart} | 15 ++++++++--- v4_devnotes.md | 3 +++ 7 files changed, 68 insertions(+), 26 deletions(-) rename packages/graphql/test/cache/{normalization.dart => normalization_test.dart} (84%) diff --git a/packages/graphql/lib/src/cache/_optimistic_transactions.dart b/packages/graphql/lib/src/cache/_optimistic_transactions.dart index 3457edcd7..08cc35133 100644 --- a/packages/graphql/lib/src/cache/_optimistic_transactions.dart +++ b/packages/graphql/lib/src/cache/_optimistic_transactions.dart @@ -1,6 +1,7 @@ /// Optimistic proxying and patching classes and typedefs used by `./cache.dart` import 'dart:collection'; +import 'package:graphql/internal.dart'; import 'package:meta/meta.dart'; import 'package:graphql/src/cache/normalizing_data_proxy.dart'; @@ -43,8 +44,20 @@ class OptimisticProxy extends NormalizingDataProxy { return data[rootId] ?? cache.readNormalized(rootId, optimistic: true); } - @override - void writeNormalized(String dataId, dynamic value) => data[dataId] = value; + // TODO consider using store for optimistic patches + /// Write normalized data into the patch, + /// deeply merging maps with existing values + /// + /// Called from [writeQuery] and [writeFragment]. + void writeNormalized(String dataId, dynamic value) { + if (value is Map) { + final existing = data[dataId]; + data[dataId] = + existing != null ? deeplyMergeLeft([existing, value]) : value; + } else { + data[dataId] = value; + } + } OptimisticPatch asPatch(String id) => OptimisticPatch(id, data); } diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index b7e62d2f0..08bd1c746 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -5,6 +5,7 @@ import 'package:graphql/src/utilities/helpers.dart'; import 'package:graphql/src/cache/store.dart'; import 'package:graphql/src/cache/_optimistic_transactions.dart'; +import 'package:normalize/normalize.dart'; export 'package:graphql/src/cache/store.dart'; export 'package:graphql/src/cache/data_proxy.dart'; @@ -13,13 +14,18 @@ class GraphQLCache extends NormalizingDataProxy { GraphQLCache({ Store store, this.dataIdFromObject, + this.addTypename = true, + this.typePolicies = const {}, }) : store = store ?? InMemoryStore(); /// Stores the underlying normalized data @protected final Store store; + /// `typePolicies` to pass down to `normalize` + final Map typePolicies; final DataIdResolver dataIdFromObject; + final bool addTypename; /// List of patches recorded through [recordOptimisticTransaction] /// @@ -52,11 +58,25 @@ class GraphQLCache extends NormalizingDataProxy { } } } + return value; } - void writeNormalized(String dataId, dynamic value) => + /// Write normalized data into the cache, + /// deeply merging maps with existing values + /// + /// Called from [writeQuery] and [writeFragment]. + void writeNormalized(String dataId, dynamic value) { + if (value is Map) { + final existing = store.get(dataId); + store.put( + dataId, + existing != null ? deeplyMergeLeft([existing, value]) : value, + ); + } else { store.put(dataId, value); + } + } String _parentPatchId(String id) { final List parts = id.split('.'); diff --git a/packages/graphql/lib/src/cache/normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/normalizing_data_proxy.dart index ee20ac74d..3bcb2327c 100644 --- a/packages/graphql/lib/src/cache/normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/normalizing_data_proxy.dart @@ -19,6 +19,8 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { /// Whether to add `__typenames` automatically bool addTypename; + bool get _addTypename => addTypename ?? true; + /// Optional `dataIdFromObject` function to pass through to [normalize] DataIdResolver dataIdFromObject; @@ -33,7 +35,8 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { /// Write normalized data into the cache. /// - /// Called from [writeQuery] and [writeFragment] + /// Called from [writeQuery] and [writeFragment]. + /// Implementors are expected to handle deep merging results themselves @protected void writeNormalized(String dataId, dynamic value); @@ -47,7 +50,8 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { operationName: request.operation.operationName, variables: request.variables, typePolicies: typePolicies, - addTypename: addTypename, + addTypename: _addTypename, + returnPartialData: true, ); Map readFragment({ @@ -64,7 +68,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { fragmentName: fragmentName, variables: variables, typePolicies: typePolicies, - addTypename: addTypename, + addTypename: _addTypename, dataIdFromObject: dataIdFromObject, ); diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index 139f7ae51..415e15fc1 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -44,6 +44,7 @@ Map _recursivelyAddAll( value, ); } else { + // Lists and nulls overwrite target as if they were normal scalars target[key] = value; } }); diff --git a/packages/graphql/test/cache/normalization_data.dart b/packages/graphql/test/cache/normalization_data.dart index 212c2551d..7fdcc202d 100644 --- a/packages/graphql/test/cache/normalization_data.dart +++ b/packages/graphql/test/cache/normalization_data.dart @@ -6,17 +6,13 @@ import 'package:meta/meta.dart'; const String rawOperationKey = 'rawOperationKey'; class TestCase { - TestCase( - String operationName, { + TestCase({ @required this.data, @required String operation, Map variables = const {}, this.normalizedEntities, }) : request = Request( - operation: Operation( - document: parseString(operation), - operationName: operationName, - ), + operation: Operation(document: parseString(operation)), variables: variables, context: Context(), ); @@ -31,7 +27,6 @@ class TestCase { } final basicTest = TestCase( - 'basicTestOperation', operation: r'''{ a { __typename @@ -83,12 +78,12 @@ final basicTest = TestCase( 'id': 9, 'dField': {'field': true} }, + 'aField': {'field': false} }, - 'aField': {'field': false} }, ); -final Map updatedCValue = { +final updatedCValue = { '__typename': 'C', 'id': 6, 'new': 'field', @@ -111,7 +106,6 @@ final Map updatedCBasicTestData = deeplyMergeLeft([ ]); final basicTestSubsetAValue = TestCase( - 'basicTestSubsetAValue', operation: r'''{ a { __typename @@ -149,7 +143,7 @@ final Map updatedSubsetOperationData = { 'a': { '__typename': 'A', 'id': 1, - 'list': basicTest.data['a']['list'], + 'list': basicTestSubsetAValue.data['a']['list'], 'b': { '__typename': 'B', 'id': 5, @@ -164,11 +158,11 @@ final Map updatedSubsetOperationData = { 'id': 10, 'dField': {'field': true} }, + 'aField': {'field': false} }, - 'aField': {'field': false} }; -final cyclicalTest = TestCase('cyclicalTestOperation', operation: r'''{ +final cyclicalTest = TestCase(operation: r'''{ a { __typename id @@ -211,9 +205,9 @@ final cyclicalTest = TestCase('cyclicalTestOperation', operation: r'''{ }, ]); -Map get cyclicalObjOperationData { - Map a; - Map b; +Map get cyclicalObjOperationData { + Map a; + Map b; a = { '__typename': 'A', 'id': 1, diff --git a/packages/graphql/test/cache/normalization.dart b/packages/graphql/test/cache/normalization_test.dart similarity index 84% rename from packages/graphql/test/cache/normalization.dart rename to packages/graphql/test/cache/normalization_test.dart index 3a0b51320..87e0512cc 100644 --- a/packages/graphql/test/cache/normalization.dart +++ b/packages/graphql/test/cache/normalization_test.dart @@ -10,7 +10,10 @@ void main() { final GraphQLCache cache = getTestCache(); test('.writeQuery .readQuery round trip', () { cache.writeQuery(basicTest.request, basicTest.data); - expect(cache.readQuery(basicTest.request), equals(basicTest.data)); + expect( + cache.readQuery(basicTest.request), + equals(basicTest.data), + ); }); test('updating nested normalized data changes top level operation', () { cache.writeNormalized('C:6', updatedCValue); @@ -21,9 +24,13 @@ void main() { }); test('updating subset query only partially overrides superset query', () { cache.writeQuery( - basicTestSubsetAValue.request, basicTestSubsetAValue.data); - expect(cache.readQuery(basicTest.request), - equals(updatedSubsetOperationData)); + basicTestSubsetAValue.request, + basicTestSubsetAValue.data, + ); + expect( + cache.readQuery(basicTest.request), + equals(updatedSubsetOperationData), + ); }); }); diff --git a/v4_devnotes.md b/v4_devnotes.md index 58a404905..39735e3b6 100644 --- a/v4_devnotes.md +++ b/v4_devnotes.md @@ -35,3 +35,6 @@ pagination works by taking multiple data events and running user defined update you can give mutations the same fetch policies queries, mutations and subscriptions all run through the same controller + + +note: contribute better error messages on `operationName != operation.name` to normalize From 3ac5e44567583a17af3b6ab9b702510bf030af6a Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 14 May 2020 14:36:29 -0500 Subject: [PATCH 015/118] caching sufficiently tested --- .../lib/src/cache/normalizing_data_proxy.dart | 7 +- ...ormalization_data.dart => cache_data.dart} | 9 + .../test/cache/graphql_cache_test.dart | 143 ++++++++++++ .../test/cache/normalization_test.dart | 59 ----- packages/graphql/test/cache/optimism.dart | 215 ------------------ packages/graphql/test/helpers.dart | 6 +- 6 files changed, 163 insertions(+), 276 deletions(-) rename packages/graphql/test/cache/{normalization_data.dart => cache_data.dart} (97%) create mode 100644 packages/graphql/test/cache/graphql_cache_test.dart delete mode 100644 packages/graphql/test/cache/normalization_test.dart delete mode 100644 packages/graphql/test/cache/optimism.dart diff --git a/packages/graphql/lib/src/cache/normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/normalizing_data_proxy.dart index 3bcb2327c..96ff36ed0 100644 --- a/packages/graphql/lib/src/cache/normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/normalizing_data_proxy.dart @@ -21,6 +21,10 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { bool get _addTypename => addTypename ?? true; + /// Used for testing + @protected + bool get returnPartialData => false; + /// Optional `dataIdFromObject` function to pass through to [normalize] DataIdResolver dataIdFromObject; @@ -51,7 +55,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { variables: request.variables, typePolicies: typePolicies, addTypename: _addTypename, - returnPartialData: true, + returnPartialData: returnPartialData, ); Map readFragment({ @@ -70,6 +74,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { typePolicies: typePolicies, addTypename: _addTypename, dataIdFromObject: dataIdFromObject, + returnPartialData: returnPartialData, ); void writeQuery( diff --git a/packages/graphql/test/cache/normalization_data.dart b/packages/graphql/test/cache/cache_data.dart similarity index 97% rename from packages/graphql/test/cache/normalization_data.dart rename to packages/graphql/test/cache/cache_data.dart index 7fdcc202d..993506b40 100644 --- a/packages/graphql/test/cache/normalization_data.dart +++ b/packages/graphql/test/cache/cache_data.dart @@ -83,6 +83,15 @@ final basicTest = TestCase( }, ); +final updatedCFragment = parseString(r''' +fragment partialC on C { + __typename + id + new + cField +} +'''); + final updatedCValue = { '__typename': 'C', 'id': 6, diff --git a/packages/graphql/test/cache/graphql_cache_test.dart b/packages/graphql/test/cache/graphql_cache_test.dart new file mode 100644 index 000000000..d47786ce8 --- /dev/null +++ b/packages/graphql/test/cache/graphql_cache_test.dart @@ -0,0 +1,143 @@ +import 'package:test/test.dart'; + +import 'package:graphql/src/cache/cache.dart'; + +import '../helpers.dart'; +import './cache_data.dart'; + +void main() { + group('Normalizes writes', () { + final GraphQLCache cache = getTestCache(); + test('.writeQuery .readQuery round trip', () { + cache.writeQuery(basicTest.request, basicTest.data); + expect( + cache.readQuery(basicTest.request), + equals(basicTest.data), + ); + }); + test('updating nested normalized fragment changes top level operation', () { + cache.writeFragment( + fragment: updatedCFragment, + idFields: { + '__typename': updatedCValue['__typename'], + 'id': updatedCValue['id'], + }, + data: updatedCValue, + ); + expect( + cache.readQuery(basicTest.request), + equals(updatedCBasicTestData), + ); + }); + + test('updating subset query only partially overrides superset query', () { + cache.writeQuery( + basicTestSubsetAValue.request, + basicTestSubsetAValue.data, + ); + expect( + cache.readQuery(basicTest.request), + equals(updatedSubsetOperationData), + ); + }); + }); + + group('Handles cyclical references', () { + final GraphQLCache cache = getTestCache(); + test('lazily reads cyclical references', () { + cache.writeQuery(cyclicalTest.request, cyclicalTest.data); + for (final normalized in cyclicalTest.normalizedEntities) { + final dataId = "${normalized['__typename']}:${normalized['id']}"; + expect(cache.readNormalized(dataId), equals(normalized)); + } + }); + }); + + group('Handles Object/pointer self-references/cycles', () { + final GraphQLCache cache = getTestCache(); + test('correctly reads cyclical references', () { + cyclicalTest.data = cyclicalObjOperationData; + cache.writeQuery(cyclicalTest.request, cyclicalTest.data); + for (final normalized in cyclicalTest.normalizedEntities) { + final dataId = "${normalized['__typename']}:${normalized['id']}"; + expect(cache.readNormalized(dataId), equals(normalized)); + } + }); + }); + + group( + '.recordOptimisticTransaction', + () { + final GraphQLCache cache = getTestCache(); + test( + '.writeQuery, .readQuery(optimistic: true) round trip', + () { + cache.recordOptimisticTransaction( + (proxy) => proxy + ..writeQuery( + basicTest.request, + basicTest.data, + ), + '1', + ); + expect( + cache.readQuery(basicTest.request, optimistic: true), + equals(basicTest.data), + ); + }, + ); + + test( + 'updating nested normalized fragment changes top level operation', + () { + cache.recordOptimisticTransaction( + (proxy) => proxy + ..writeFragment( + fragment: updatedCFragment, + idFields: { + '__typename': updatedCValue['__typename'], + 'id': updatedCValue['id'], + }, + data: updatedCValue, + ), + '2', + ); + expect( + cache.readQuery(basicTest.request), + equals(updatedCBasicTestData), + ); + }, + ); + + test( + 'updating subset query only partially overrides superset query', + () { + cache.recordOptimisticTransaction( + (proxy) => proxy + ..writeQuery( + basicTestSubsetAValue.request, + basicTestSubsetAValue.data, + ), + '3', + ); + expect( + cache.readQuery(basicTest.request, optimistic: true), + equals(updatedSubsetOperationData), + ); + }, + ); + + test( + '.removeOptimisticPatch results in data from lower layers on readQuery', + () { + cache.removeOptimisticPatch('2'); + cache.removeOptimisticPatch('3'); + expect( + cache.readQuery(basicTest.request, optimistic: true), + equals(basicTest.data), + ); + }, + ); + }, + ); +} diff --git a/packages/graphql/test/cache/normalization_test.dart b/packages/graphql/test/cache/normalization_test.dart deleted file mode 100644 index 87e0512cc..000000000 --- a/packages/graphql/test/cache/normalization_test.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:test/test.dart'; - -import 'package:graphql/src/cache/cache.dart'; - -import '../helpers.dart'; -import 'normalization_data.dart'; - -void main() { - group('Normalizes writes', () { - final GraphQLCache cache = getTestCache(); - test('.writeQuery .readQuery round trip', () { - cache.writeQuery(basicTest.request, basicTest.data); - expect( - cache.readQuery(basicTest.request), - equals(basicTest.data), - ); - }); - test('updating nested normalized data changes top level operation', () { - cache.writeNormalized('C:6', updatedCValue); - expect( - cache.readQuery(basicTest.request), - equals(updatedCBasicTestData), - ); - }); - test('updating subset query only partially overrides superset query', () { - cache.writeQuery( - basicTestSubsetAValue.request, - basicTestSubsetAValue.data, - ); - expect( - cache.readQuery(basicTest.request), - equals(updatedSubsetOperationData), - ); - }); - }); - - group('Handles cyclical references', () { - final GraphQLCache cache = getTestCache(); - test('lazily reads cyclical references', () { - cache.writeQuery(cyclicalTest.request, cyclicalTest.data); - for (final normalized in cyclicalTest.normalizedEntities) { - final dataId = "${normalized['__typename']}:${normalized['id']}"; - expect(cache.readNormalized(dataId), equals(normalized)); - } - }); - }); - - group('Handles Object/pointer self-references/cycles', () { - final GraphQLCache cache = getTestCache(); - test('correctly reads cyclical references', () { - cyclicalTest.data = cyclicalObjOperationData; - cache.writeQuery(cyclicalTest.request, cyclicalTest.data); - for (final normalized in cyclicalTest.normalizedEntities) { - final dataId = "${normalized['__typename']}:${normalized['id']}"; - expect(cache.readNormalized(dataId), equals(normalized)); - } - }); - }); -} diff --git a/packages/graphql/test/cache/optimism.dart b/packages/graphql/test/cache/optimism.dart deleted file mode 100644 index de6eebcb4..000000000 --- a/packages/graphql/test/cache/optimism.dart +++ /dev/null @@ -1,215 +0,0 @@ -/* -import 'package:test/test.dart'; - -import 'package:graphql/src/cache/normalized_in_memory.dart' - show typenameDataIdFromObject; -import 'package:graphql/src/cache/optimistic.dart'; -import 'package:graphql/src/cache/lazy_cache_map.dart'; - -List reference(String key) { - return ['cache/reference', key]; -} - -const String rawOperationKey = 'rawOperationKey'; - -final Map rawOperationData = { - 'a': { - '__typename': 'A', - 'id': 1, - 'list': [ - 1, - 2, - 3, - { - '__typename': 'Item', - 'id': 4, - 'value': 4, - } - ], - 'b': { - '__typename': 'B', - 'id': 5, - 'c': { - '__typename': 'C', - 'id': 6, - 'cField': 'value', - }, - 'bField': {'field': true} - }, - 'd': { - 'id': 9, - 'dField': {'field': true} - }, - }, - 'aField': {'field': false} -}; - -final Map updatedCValue = { - '__typename': 'C', - 'id': 6, - 'new': 'field', - 'cField': 'changed value', -}; - -final Map updatedCOperationData = { - 'a': { - '__typename': 'A', - 'id': 1, - 'list': [ - 1, - 2, - 3, - { - '__typename': 'Item', - 'id': 4, - 'value': 4, - } - ], - 'b': { - '__typename': 'B', - 'id': 5, - 'c': updatedCValue, - 'bField': {'field': true} - }, - 'd': { - 'id': 9, - 'dField': {'field': true} - }, - }, - 'aField': {'field': false} -}; - -final Map subsetAValue = { - 'a': { - '__typename': 'A', - 'id': 1, - 'list': [ - 5, - 6, - 7, - { - '__typename': 'Item', - 'id': 8, - 'value': 8, - } - ], - 'd': { - 'id': 10, - }, - }, -}; - -final Map updatedSubsetOperationData = { - 'a': { - '__typename': 'A', - 'id': 1, - 'list': [ - 5, - 6, - 7, - { - '__typename': 'Item', - 'id': 8, - 'value': 8, - } - ], - 'b': { - '__typename': 'B', - 'id': 5, - 'c': updatedCValue, - 'bField': {'field': true} - }, - 'd': { - 'id': 10, - 'dField': {'field': true} - }, - }, - 'aField': {'field': false} -}; - -Map get cyclicalOperationData { - Map a; - Map b; - a = { - '__typename': 'A', - 'id': 1, - }; - b = { - '__typename': 'B', - 'id': 5, - 'as': [a] - }; - a['b'] = b; - return {'a': a}; -} - -final Map cyclicalNormalizedA = { - '__typename': 'A', - 'id': 1, - 'b': ['@cache/reference', 'B/5'], -}; - -final Map cyclicalNormalizedB = { - '__typename': 'B', - 'id': 5, - 'as': [ - ['@cache/reference', 'A/1'] - ], -}; - -OptimisticCache getTestCache() => OptimisticCache( - dataIdFromObject: typenameDataIdFromObject, - ); - -void main() { - group('Normalizes writes', () { - final OptimisticCache cache = getTestCache(); - test('lazily reads cyclical references', () { - cache.write(rawOperationKey, cyclicalOperationData); - final LazyCacheMap a = cache.read('A/1') as LazyCacheMap; - expect(a.data, equals(cyclicalNormalizedA)); - final LazyCacheMap b = a['b'] as LazyCacheMap; - expect(b.data, equals(cyclicalNormalizedB)); - }); - }); - - group('Normalizes writes optimistically', () { - final OptimisticCache cache = getTestCache(); - test('lazily reads cyclical references', () { - cache.addOptimisiticPatch(rawOperationKey, - (cache) => cache..write(rawOperationKey, cyclicalOperationData)); - final LazyCacheMap a = cache.read('A/1') as LazyCacheMap; - expect(a.data, equals(cyclicalNormalizedA)); - final LazyCacheMap b = a['b'] as LazyCacheMap; - expect(b.data, equals(cyclicalNormalizedB)); - }); - }); - - group('Optimistic writes', () { - final OptimisticCache cache = getTestCache(); - test('.addOptimisiticPatch .readDenormalize round trip', () { - cache.addOptimisiticPatch( - rawOperationKey, - (cache) => cache..write(rawOperationKey, rawOperationData), - ); - expect(cache.denormalizedRead(rawOperationKey), equals(rawOperationData)); - }); - test('updating nested data changes top level optimistic operation', () { - cache.addOptimisiticPatch( - '$rawOperationKey.C', - (cache) => cache..write('C/6', updatedCValue), - ); - expect( - cache.denormalizedRead(rawOperationKey), - equals(updatedCOperationData), - ); - }); - test('removing optimistic patch clears results', () { - cache.removeOptimisticPatch(rawOperationKey); - expect(cache.read(rawOperationKey), equals(null)); - expect(cache.read('C/6'), equals(null)); - }); - }); -} - -*/ diff --git a/packages/graphql/test/helpers.dart b/packages/graphql/test/helpers.dart index a9f37d7ca..4dbdefc89 100644 --- a/packages/graphql/test/helpers.dart +++ b/packages/graphql/test/helpers.dart @@ -10,4 +10,8 @@ overridePrint(testFn(List log)) => () { return Zone.current.fork(specification: spec).run(() => testFn(log)); }; -GraphQLCache getTestCache() => GraphQLCache(); +class TestCache extends GraphQLCache { + bool get returnPartialData => true; +} + +GraphQLCache getTestCache() => TestCache(); From d9452bc4529d261ca74b17a9be1baaaf231dcea2 Mon Sep 17 00:00:00 2001 From: micimize Date: Sun, 17 May 2020 13:49:27 -0500 Subject: [PATCH 016/118] feat: starting on gql links --- packages/graphql/lib/client.dart | 4 +--- ...alizing_data_proxy.dart => _normalizing_data_proxy.dart} | 0 .../graphql/lib/src/cache/_optimistic_transactions.dart | 2 +- packages/graphql/lib/src/cache/cache.dart | 2 +- packages/graphql/lib/src/links/gql_links.dart | 6 ++++++ packages/graphql/lib/src/links/links.dart | 4 ++++ packages/graphql/pubspec.yaml | 6 ++++++ 7 files changed, 19 insertions(+), 5 deletions(-) rename packages/graphql/lib/src/cache/{normalizing_data_proxy.dart => _normalizing_data_proxy.dart} (100%) create mode 100644 packages/graphql/lib/src/links/gql_links.dart create mode 100644 packages/graphql/lib/src/links/links.dart diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index 5abf68e2d..651d5df8e 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -7,6 +7,4 @@ export 'package:graphql/src/core/query_result.dart'; export 'package:graphql/src/exceptions/exceptions.dart'; export 'package:graphql/src/graphql_client.dart'; -export 'package:graphql/src/links/auth_link.dart'; -export 'package:gql_link/gql_link.dart'; -export 'package:gql_http_link/gql_http_link.dart'; +export 'package:graphql/src/links/links.dart'; diff --git a/packages/graphql/lib/src/cache/normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart similarity index 100% rename from packages/graphql/lib/src/cache/normalizing_data_proxy.dart rename to packages/graphql/lib/src/cache/_normalizing_data_proxy.dart diff --git a/packages/graphql/lib/src/cache/_optimistic_transactions.dart b/packages/graphql/lib/src/cache/_optimistic_transactions.dart index 08cc35133..7eb552c17 100644 --- a/packages/graphql/lib/src/cache/_optimistic_transactions.dart +++ b/packages/graphql/lib/src/cache/_optimistic_transactions.dart @@ -4,7 +4,7 @@ import 'dart:collection'; import 'package:graphql/internal.dart'; import 'package:meta/meta.dart'; -import 'package:graphql/src/cache/normalizing_data_proxy.dart'; +import 'package:graphql/src/cache/_normalizing_data_proxy.dart'; import 'package:graphql/src/cache/data_proxy.dart'; import 'package:graphql/src/cache/cache.dart' show GraphQLCache; diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index 08bd1c746..849c6f723 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -1,4 +1,4 @@ -import 'package:graphql/src/cache/normalizing_data_proxy.dart'; +import 'package:graphql/src/cache/_normalizing_data_proxy.dart'; import 'package:meta/meta.dart'; import 'package:graphql/src/utilities/helpers.dart'; diff --git a/packages/graphql/lib/src/links/gql_links.dart b/packages/graphql/lib/src/links/gql_links.dart new file mode 100644 index 000000000..9b5a32a2c --- /dev/null +++ b/packages/graphql/lib/src/links/gql_links.dart @@ -0,0 +1,6 @@ +export 'package:gql_link/gql_link.dart'; + +export 'package:gql_http_link/gql_http_link.dart'; +export 'package:gql_websocket_link/gql_websocket_link.dart'; +export 'package:gql_error_link/gql_error_link.dart'; +export 'package:gql_dedupe_link/gql_dedupe_link.dart'; diff --git a/packages/graphql/lib/src/links/links.dart b/packages/graphql/lib/src/links/links.dart new file mode 100644 index 000000000..ce0e0a0b2 --- /dev/null +++ b/packages/graphql/lib/src/links/links.dart @@ -0,0 +1,4 @@ +// Reexport all gql_links +export 'package:graphql/src/links/gql_links.dart'; + +export 'package:graphql/src/links/auth_link.dart'; diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index cbd19bef5..5c2c1bf0c 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -16,9 +16,14 @@ dependencies: gql_exec: ^0.2.2 gql_link: ^0.3.0 gql_http_link: ^0.2.9 + gql_websocket_link: ^0.1.1-alpha gql_transform_link: ^0.1.5 + gql_error_link: ^0.1.1-alpha + gql_dedupe_link: ^1.0.10 + quiver: ">=2.0.0 <3.0.0" normalize: ^0.2.0 + dev_dependencies: pedantic: ^1.8.0+1 mockito: ^4.0.0 @@ -26,3 +31,4 @@ dev_dependencies: test_coverage: ^0.3.0+1 environment: sdk: ">=2.6.0 <3.0.0" + From 0d7ef7a885d905592dee313a64e57505dc5d7973 Mon Sep 17 00:00:00 2001 From: micimize Date: Tue, 19 May 2020 10:26:34 -0500 Subject: [PATCH 017/118] feat: more work on gql links --- packages/graphql/lib/src/links/auth_link.dart | 6 +- .../graphql/lib/src/links/error_link.dart | 77 ------------------- 2 files changed, 3 insertions(+), 80 deletions(-) delete mode 100644 packages/graphql/lib/src/links/error_link.dart diff --git a/packages/graphql/lib/src/links/auth_link.dart b/packages/graphql/lib/src/links/auth_link.dart index 71653e803..272db12b5 100644 --- a/packages/graphql/lib/src/links/auth_link.dart +++ b/packages/graphql/lib/src/links/auth_link.dart @@ -4,11 +4,11 @@ import 'package:meta/meta.dart'; import "package:gql_exec/gql_exec.dart"; import "package:gql_http_link/gql_http_link.dart"; import "package:gql_link/gql_link.dart"; +import "package:gql_error_link/gql_error_link.dart"; import "package:gql_transform_link/gql_transform_link.dart"; -import "./error_link.dart"; - -// TODO temporarily taken from gql https://github.com/gql-dart/gql/pull/103 +// Mostly taken from +// https://github.com/gql-dart/gql/blob/master/examples/gql_example_http_auth_link/lib/http_auth_link.dart class AuthLink extends Link { Link _link; String _token; diff --git a/packages/graphql/lib/src/links/error_link.dart b/packages/graphql/lib/src/links/error_link.dart deleted file mode 100644 index a029d5e7e..000000000 --- a/packages/graphql/lib/src/links/error_link.dart +++ /dev/null @@ -1,77 +0,0 @@ -// TODO temporarily taken from gql https://github.com/gql-dart/gql/pull/103 -import "dart:async"; -import "package:async/async.dart"; -import "package:gql_link/gql_link.dart"; -import "package:gql_exec/gql_exec.dart"; - -/// A handler of GraphQL errors. -typedef ErrorHandler = Stream Function( - Request request, - NextLink forward, - Response response, -); - -/// A handler of Link Exceptions. -typedef ExceptionHandler = Stream Function( - Request request, - NextLink forward, - LinkException exception, -); - -/// [ErrorLink] allows interception of GraphQL errors (using [onGraphQLError]) -/// and [LinkException]s (using [onException]). -/// -/// In both cases [ErrorLink] transfers control over to the handler which may -/// return a new stream to discard the original stream. If the handler returns -/// `null`, the original stream is left intact and will be allowed to continue -/// streaming new events. -class ErrorLink extends Link { - final ErrorHandler onGraphQLError; - final ExceptionHandler onException; - - const ErrorLink({ - this.onGraphQLError, - this.onException, - }); - - @override - Stream request( - Request request, [ - forward, - ]) async* { - await for (final result in Result.captureStream(forward(request))) { - if (result.isError) { - final error = result.asError.error; - - if (onException != null && error is LinkException) { - final stream = onException(request, forward, error); - - if (stream != null) { - yield* stream; - - return; - } - } - - yield* Stream.error(error); - } - - if (result.isValue) { - final response = result.asValue.value; - final errors = response.errors; - - if (onGraphQLError != null && errors != null && errors.isNotEmpty) { - final stream = onGraphQLError(request, forward, response); - - if (stream != null) { - yield* stream; - - return; - } - } - - yield response; - } - } - } -} From 2c3c66cbb514a90e16a87ea4c722555824e18a06 Mon Sep 17 00:00:00 2001 From: micimize Date: Tue, 19 May 2020 10:26:44 -0500 Subject: [PATCH 018/118] feat: HiveStore --- packages/graphql/lib/src/cache/cache.dart | 3 +- .../graphql/lib/src/cache/hive_store.dart | 44 +++++++++++++++++++ packages/graphql/pubspec.yaml | 2 + 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/graphql/lib/src/cache/hive_store.dart diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index 849c6f723..2853a0ee2 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -7,8 +7,9 @@ import 'package:graphql/src/cache/store.dart'; import 'package:graphql/src/cache/_optimistic_transactions.dart'; import 'package:normalize/normalize.dart'; -export 'package:graphql/src/cache/store.dart'; export 'package:graphql/src/cache/data_proxy.dart'; +export 'package:graphql/src/cache/store.dart'; +export 'package:graphql/src/cache/hive_store.dart'; class GraphQLCache extends NormalizingDataProxy { GraphQLCache({ diff --git a/packages/graphql/lib/src/cache/hive_store.dart b/packages/graphql/lib/src/cache/hive_store.dart new file mode 100644 index 000000000..948e3062a --- /dev/null +++ b/packages/graphql/lib/src/cache/hive_store.dart @@ -0,0 +1,44 @@ +import 'package:meta/meta.dart'; + +import 'package:hive/hive.dart'; + +import './store.dart'; + +@immutable +class HiveStore extends Store { + @protected + final Box box; + + /// Creates a HiveStore inititalized with [box], + /// which defaults to `Hive.box('defaultGraphqlStore')` + HiveStore([ + Box box, + ]) : box = box ?? Hive.box('defaultGraphqlStore'); + + @override + Map get(String dataId) { + final result = box.get(dataId); + if (result == null) return null; + return Map.from(result); + } + + @override + void put(String dataId, Map value) { + box.put(dataId, value); + } + + @override + void putAll(Map> data) { + box.putAll(data); + } + + @override + void delete(String dataId) { + box.delete(dataId); + } + + @override + Map> toMap() => Map.unmodifiable(box.toMap()); + + void reset() => box.clear(); +} diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 5c2c1bf0c..c08ed7df9 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -21,6 +21,8 @@ dependencies: gql_error_link: ^0.1.1-alpha gql_dedupe_link: ^1.0.10 + hive: ^1.3.0 + quiver: ">=2.0.0 <3.0.0" normalize: ^0.2.0 From 39bde3f13893d069942e3b91cb80154978a3ce4f Mon Sep 17 00:00:00 2001 From: micimize Date: Tue, 19 May 2020 11:51:10 -0500 Subject: [PATCH 019/118] generalized adding typename --- packages/graphql/example/README.md | 2 +- packages/graphql/example/bin/main.dart | 5 ++-- packages/graphql/lib/client.dart | 2 ++ .../src/cache/_normalizing_data_proxy.dart | 23 +++++++++++-------- packages/graphql/lib/src/cache/cache.dart | 2 -- .../graphql/lib/src/cache/data_proxy.dart | 7 +----- .../graphql/lib/src/core/query_manager.dart | 21 +++++------------ .../graphql/lib/src/core/query_options.dart | 4 ---- .../graphql/lib/src/utilities/helpers.dart | 17 ++++++++++++++ 9 files changed, 42 insertions(+), 41 deletions(-) diff --git a/packages/graphql/example/README.md b/packages/graphql/example/README.md index fea472d75..7e5122862 100644 --- a/packages/graphql/example/README.md +++ b/packages/graphql/example/README.md @@ -1,4 +1,4 @@ -# Example Dart Application +# `graphql` Example Application This is a simple command line application to showcase how you can use the Dart GraphQL Client, without flutter. diff --git a/packages/graphql/example/bin/main.dart b/packages/graphql/example/bin/main.dart index 52e57e9fd..5a95b214a 100644 --- a/packages/graphql/example/bin/main.dart +++ b/packages/graphql/example/bin/main.dart @@ -1,8 +1,6 @@ import 'dart:io' show stdout, stderr, exit; import 'package:args/args.dart'; -import 'package:gql_http_link/gql_http_link.dart'; -import 'package:gql_link/gql_link.dart'; import 'package:graphql/client.dart'; import './graphql_operation/mutations/mutations.dart'; @@ -55,7 +53,8 @@ void query() async { result.data['viewer']['repositories']['nodes'] as List; repositories.forEach( - (dynamic f) => {stdout.writeln('Id: ${f['id']} Name: ${f['name']}')}); + (dynamic f) => {stdout.writeln('Id: ${f['id']} Name: ${f['name']}')}, + ); exit(0); } diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index 651d5df8e..1cfd993dd 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -8,3 +8,5 @@ export 'package:graphql/src/exceptions/exceptions.dart'; export 'package:graphql/src/graphql_client.dart'; export 'package:graphql/src/links/links.dart'; + +export 'package:graphql/src/utilities/helpers.dart' show gql; diff --git a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart index 96ff36ed0..c85b7df5f 100644 --- a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart @@ -16,10 +16,15 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { /// `typePolicies` to pass down to `normalize` Map typePolicies; - /// Whether to add `__typenames` automatically - bool addTypename; - - bool get _addTypename => addTypename ?? true; + /// Whether to add `__typename` automatically. + /// + /// This is `false` by default because [gql] automatically adds `__typename` already. + /// + /// If [addTypename] is true, it is important for the client + /// to add `__typename` to each request automatically as well. + /// Otherwise, a round trip to the cache will nullify results unless + /// [returnPartialData] is `true` + bool addTypename = false; /// Used for testing @protected @@ -54,7 +59,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { operationName: request.operation.operationName, variables: request.variables, typePolicies: typePolicies, - addTypename: _addTypename, + addTypename: addTypename ?? false, returnPartialData: returnPartialData, ); @@ -72,16 +77,15 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { fragmentName: fragmentName, variables: variables, typePolicies: typePolicies, - addTypename: _addTypename, + addTypename: addTypename ?? false, dataIdFromObject: dataIdFromObject, returnPartialData: returnPartialData, ); void writeQuery( Request request, - Map data, { - String queryId, - }) => + Map data, + ) => normalize( writer: (dataId, value) => writeNormalized(dataId, value), query: request.operation.document, @@ -98,7 +102,6 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { @required Map data, String fragmentName, Map variables, - String queryId, }) => normalizeFragment( writer: (dataId, value) => writeNormalized(dataId, value), diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index 2853a0ee2..02a64547e 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -15,7 +15,6 @@ class GraphQLCache extends NormalizingDataProxy { GraphQLCache({ Store store, this.dataIdFromObject, - this.addTypename = true, this.typePolicies = const {}, }) : store = store ?? InMemoryStore(); @@ -26,7 +25,6 @@ class GraphQLCache extends NormalizingDataProxy { /// `typePolicies` to pass down to `normalize` final Map typePolicies; final DataIdResolver dataIdFromObject; - final bool addTypename; /// List of patches recorded through [recordOptimisticTransaction] /// diff --git a/packages/graphql/lib/src/cache/data_proxy.dart b/packages/graphql/lib/src/cache/data_proxy.dart index 2c1c0b9ea..df21ea4ca 100644 --- a/packages/graphql/lib/src/cache/data_proxy.dart +++ b/packages/graphql/lib/src/cache/data_proxy.dart @@ -31,11 +31,7 @@ abstract class GraphQLDataProxy { /// /// Conceptually, this can be thought of as providing a manual execution result /// in the form of `data` - void writeQuery( - Request request, - Map data, { - String queryId, - }); + void writeQuery(Request request, Map data); /// Writes a GraphQL fragment to any arbitrary id. /// @@ -47,6 +43,5 @@ abstract class GraphQLDataProxy { @required Map data, String fragmentName, Map variables, - String queryId, }); } diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 046f0c920..cda45609a 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -120,11 +120,7 @@ class QueryManager { // save the data from response to the cache if (response.data != null && options.fetchPolicy != FetchPolicy.noCache) { - cache.writeQuery( - request, - response.data, - queryId: queryId, - ); + cache.writeQuery(request, response.data); } queryResult = mapFetchResultToQueryResult( @@ -171,7 +167,6 @@ class QueryManager { if (options.optimisticResult != null) { queryResult = _getOptimisticQueryResult( request, - queryId, cacheKey: cacheKey, optimisticResult: options.optimisticResult, ); @@ -234,7 +229,9 @@ class QueryManager { return null; } - /// Add a result to the query specified by `queryId`, if it exists + /// Add a result to the [ObservableQuery] specified by `queryId`, if it exists + /// + /// Queries are registered via [setQuery] and [watchQuery] void addQueryResult( Request request, String queryId, @@ -246,7 +243,6 @@ class QueryManager { cache.writeQuery( request, queryResult.data, - queryId: observableQuery.options.toKey(), ); } @@ -257,16 +253,11 @@ class QueryManager { /// Create an optimstic result for the query specified by `queryId`, if it exists QueryResult _getOptimisticQueryResult( - Request request, - String queryId, { + Request request, { @required String cacheKey, @required Object optimisticResult, }) { - cache.writeQuery( - request, - optimisticResult, - queryId: queryId, - ); + cache.writeQuery(request, optimisticResult); final QueryResult queryResult = QueryResult( data: cache.readQuery( diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index a57ac20cb..a4e332a91 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -2,7 +2,6 @@ import 'package:graphql/src/cache/cache.dart'; import 'package:meta/meta.dart'; import 'package:gql/ast.dart'; -import 'package:gql/language.dart'; import 'package:gql_exec/gql_exec.dart'; import 'package:graphql/client.dart'; @@ -10,9 +9,6 @@ import 'package:graphql/internal.dart'; import 'package:graphql/src/core/raw_operation_data.dart'; import 'package:graphql/src/utilities/helpers.dart'; -/// Parse GraphQL query strings into the standard GraphQL AST. -DocumentNode gql(String query) => parseString(query); - /// [FetchPolicy] determines where the client may return a result from. The options are: /// - cacheFirst (default): return result from cache. Only fetch from network if cached result is not available. /// - cacheAndNetwork: return result from cache first (if it exists), then return network result once it's available. diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index 415e15fc1..37263ed54 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -1,3 +1,7 @@ +import 'package:gql/ast.dart'; +import 'package:gql/language.dart'; +import 'package:normalize/normalize.dart'; + bool notNull(Object any) { return any != null; } @@ -70,3 +74,16 @@ Map deeplyMergeLeft( // prepend an empty literal for functional immutability return (>[{}]..addAll(maps)).reduce(_recursivelyAddAll); } + +/// Parse a GraphQL [document] into a [DocumentNode], +/// automatically adding `__typename`s +/// +/// If you want to provide your own document parser or builder, +/// keep in mind that default cache normalization depends heavily on `__typename`s, +/// So you should probably include an [AddTypenameVistor] [transform] +DocumentNode gql(String document) => transform( + parseString(document), + [ + AddTypenameVisitor(), + ], + ); From 500064aede63e5f7f6ec8c3056fa069df84e763d Mon Sep 17 00:00:00 2001 From: micimize Date: Tue, 19 May 2020 12:16:12 -0500 Subject: [PATCH 020/118] updating examples --- examples/starwars/lib/client_provider.dart | 10 +---- .../ios/Flutter/flutter_export_environment.sh | 14 ++++--- .../example/lib/fetchmore/main.dart | 2 +- .../example/lib/graphql_bloc/bloc.dart | 17 +------- .../example/lib/graphql_widget/main.dart | 39 ++++++++++++------- packages/graphql_flutter/example/pubspec.yaml | 1 - .../graphql_flutter/lib/graphql_flutter.dart | 5 +-- packages/graphql_flutter/lib/src/caches.dart | 38 ------------------ .../lib/src/widgets/cache_provider.dart | 4 +- packages/graphql_flutter/pubspec.yaml | 3 +- 10 files changed, 45 insertions(+), 88 deletions(-) delete mode 100644 packages/graphql_flutter/lib/src/caches.dart diff --git a/examples/starwars/lib/client_provider.dart b/examples/starwars/lib/client_provider.dart index e9beecf10..0ce098b32 100644 --- a/examples/starwars/lib/client_provider.dart +++ b/examples/starwars/lib/client_provider.dart @@ -12,9 +12,7 @@ String uuidFromObject(Object object) { return null; } -final OptimisticCache cache = OptimisticCache( - dataIdFromObject: uuidFromObject, -); +final GraphQLCache cache = GraphQLCache(); ValueNotifier clientFor({ @required String uri, @@ -23,11 +21,7 @@ ValueNotifier clientFor({ Link link = HttpLink(uri: uri); if (subscriptionUri != null) { final WebSocketLink websocketLink = WebSocketLink( - url: subscriptionUri, - config: SocketClientConfig( - autoReconnect: true, - inactivityTimeout: Duration(seconds: 30), - ), + subscriptionUri, ); link = link.concat(websocketLink); diff --git a/packages/graphql_flutter/example/ios/Flutter/flutter_export_environment.sh b/packages/graphql_flutter/example/ios/Flutter/flutter_export_environment.sh index b6b4fa344..2720650c7 100755 --- a/packages/graphql_flutter/example/ios/Flutter/flutter_export_environment.sh +++ b/packages/graphql_flutter/example/ios/Flutter/flutter_export_environment.sh @@ -1,10 +1,14 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=C:\tools\flutter" -export "FLUTTER_APPLICATION_PATH=C:\Users\mw\projects\opensource\graphql-flutter\packages\graphql_flutter\example" -export "FLUTTER_TARGET=lib\main.dart" +export "FLUTTER_ROOT=/Users/mjr/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/mjr/Documents/code/libraries/dart/graphql-flutter/packages/graphql_flutter/example" +export "FLUTTER_TARGET=lib/main.dart" export "FLUTTER_BUILD_DIR=build" -export "SYMROOT=${SOURCE_ROOT}/../build\ios" -export "FLUTTER_FRAMEWORK_DIR=C:\tools\flutter\bin\cache\artifacts\engine\ios" +export "SYMROOT=${SOURCE_ROOT}/../build/ios" +export "OTHER_LDFLAGS=$(inherited) -framework Flutter" +export "FLUTTER_FRAMEWORK_DIR=/Users/mjr/flutter/bin/cache/artifacts/engine/ios" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=false" +export "TREE_SHAKE_ICONS=false" diff --git a/packages/graphql_flutter/example/lib/fetchmore/main.dart b/packages/graphql_flutter/example/lib/fetchmore/main.dart index ded8b6b5f..c9e55f930 100644 --- a/packages/graphql_flutter/example/lib/fetchmore/main.dart +++ b/packages/graphql_flutter/example/lib/fetchmore/main.dart @@ -15,7 +15,7 @@ class FetchMoreWidgetScreen extends StatelessWidget { final authLink = AuthLink( // ignore: undefined_identifier - getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', + getToken: () => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', ); final link = authLink.concat(httpLink); diff --git a/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart b/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart index 638456773..d2ca745c6 100644 --- a/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart +++ b/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart @@ -1,4 +1,3 @@ -import 'package:gql/language.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:rxdart/subjects.dart'; @@ -65,9 +64,7 @@ class Bloc { static final Link _link = _authLink.concat(_httpLink); static final GraphQLClient _client = GraphQLClient( - cache: NormalizedInMemoryCache( - dataIdFromObject: typenameDataIdFromObject, - ), + cache: GraphQLCache(), link: _link, ); @@ -78,8 +75,6 @@ class Bloc { variables: { 'starrableId': repo.id, }, -// fetchPolicy: widget.options.fetchPolicy, -// errorPolicy: widget.options.errorPolicy, ); final result = await _client.mutate(_options); @@ -89,21 +84,13 @@ class Bloc { Future _queryRepo({int nRepositories = 50}) async { // null is loading _repoSubject.add(null); -// FetchPolicy fetchPolicy = widget.options.fetchPolicy; -// -// if (fetchPolicy == FetchPolicy.cacheFirst) { -// fetchPolicy = FetchPolicy.cacheAndNetwork; -// } final _options = WatchQueryOptions( - document: parseString(queries.readRepositories), + document: gql(queries.readRepositories), variables: { 'nRepositories': nRepositories, }, -// fetchPolicy: fetchPolicy, -// errorPolicy: widget.options.errorPolicy, pollInterval: 4, fetchResults: true, -// context: widget.options.context, ); final result = await _client.query(_options); diff --git a/packages/graphql_flutter/example/lib/graphql_widget/main.dart b/packages/graphql_flutter/example/lib/graphql_widget/main.dart index 63bd8f0a4..9b85ff1a3 100644 --- a/packages/graphql_flutter/example/lib/graphql_widget/main.dart +++ b/packages/graphql_flutter/example/lib/graphql_widget/main.dart @@ -27,20 +27,14 @@ class GraphQLWidgetScreen extends StatelessWidget { var link = authLink.concat(httpLink); if (ENABLE_WEBSOCKETS) { - final websocketLink = WebSocketLink( - url: 'ws://localhost:8080/ws/graphql', - config: SocketClientConfig( - autoReconnect: true, inactivityTimeout: Duration(seconds: 15)), - ); + final websocketLink = WebSocketLink('ws://localhost:8080/ws/graphql'); link = link.concat(websocketLink); } final client = ValueNotifier( GraphQLClient( - cache: OptimisticCache( - dataIdFromObject: typenameDataIdFromObject, - ), + cache: GraphQLCache(), link: link, ), ); @@ -111,15 +105,17 @@ class _MyHomePageState extends State { // result.data can be either a [List] or a [Map] final repositories = (result.data['viewer']['repositories'] - ['nodes'] as List) - .cast(); + ['nodes'] as List); return Expanded( child: ListView.builder( itemCount: repositories.length, itemBuilder: (BuildContext context, int index) { return StarrableRepository( - repository: repositories[index]); + repository: repositories[index], + optimistic: result.source == + QueryResultSource.OptimisticResult, + ); }, ), ); @@ -149,9 +145,11 @@ class StarrableRepository extends StatelessWidget { const StarrableRepository({ Key key, @required this.repository, + @required this.optimistic, }) : super(key: key); final Map repository; + final bool optimistic; Map extractRepositoryData(Object data) { final action = @@ -163,7 +161,6 @@ class StarrableRepository extends StatelessWidget { } bool get starred => repository['viewerHasStarred'] as bool; - bool get optimistic => (repository as LazyCacheMap).isOptimistic; Map get expectedResult => { 'action': { @@ -176,13 +173,27 @@ class StarrableRepository extends StatelessWidget { return Mutation( options: MutationOptions( document: gql(starred ? mutations.removeStar : mutations.addStar), - update: (Cache cache, QueryResult result) { + update: (cache, QueryResult result) { if (result.hasException) { print(result.exception); } else { final updated = Map.from(repository) ..addAll(extractRepositoryData(result.data)); - cache.write(typenameDataIdFromObject(updated), updated); + cache.writeFragment( + fragment: gql(''' + fragment fields on Repository { + __typename + id + name + viewerHasStarred + } + '''), + idFields: { + '__typename': updated['__typename'], + 'id': updated['id'], + }, + data: updated, + ); } }, onError: (OperationException error) { diff --git a/packages/graphql_flutter/example/pubspec.yaml b/packages/graphql_flutter/example/pubspec.yaml index d36555a1b..963327c1f 100644 --- a/packages/graphql_flutter/example/pubspec.yaml +++ b/packages/graphql_flutter/example/pubspec.yaml @@ -8,7 +8,6 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.2 - rxdart: ^0.23.1 connectivity: ^0.4.4 graphql_flutter: path: .. diff --git a/packages/graphql_flutter/lib/graphql_flutter.dart b/packages/graphql_flutter/lib/graphql_flutter.dart index 537e0a0ce..fbb9671ac 100644 --- a/packages/graphql_flutter/lib/graphql_flutter.dart +++ b/packages/graphql_flutter/lib/graphql_flutter.dart @@ -1,9 +1,6 @@ library graphql_flutter; -export 'package:graphql/client.dart' - hide GraphQLCache, NormalizedInMemoryCache, OptimisticCache; - -export 'package:graphql_flutter/src/caches.dart'; +export 'package:graphql/client.dart'; export 'package:graphql_flutter/src/widgets/cache_provider.dart'; export 'package:graphql_flutter/src/widgets/graphql_consumer.dart'; diff --git a/packages/graphql_flutter/lib/src/caches.dart b/packages/graphql_flutter/lib/src/caches.dart deleted file mode 100644 index ae1efe629..000000000 --- a/packages/graphql_flutter/lib/src/caches.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'dart:async' show FutureOr; - -import 'package:meta/meta.dart'; -import 'package:path_provider/path_provider.dart' - show getApplicationDocumentsDirectory; - -import 'package:graphql/client.dart' as client; - -final FutureOr flutterStoragePrefix = - (() async => (await getApplicationDocumentsDirectory()).path)(); - -class GraphQLCache extends client.GraphQLCache { - GraphQLCache({ - FutureOr storagePrefix, - }) : super(storagePrefix: storagePrefix ?? flutterStoragePrefix); -} - -class NormalizedInMemoryCache extends client.NormalizedInMemoryCache { - NormalizedInMemoryCache({ - @required client.DataIdFromObject dataIdFromObject, - String prefix = '@cache/reference', - }) : super( - dataIdFromObject: dataIdFromObject, - prefix: prefix, - storagePrefix: flutterStoragePrefix, - ); -} - -class OptimisticCache extends client.OptimisticCache { - OptimisticCache({ - @required client.DataIdFromObject dataIdFromObject, - String prefix = '@cache/reference', - }) : super( - dataIdFromObject: dataIdFromObject, - prefix: prefix, - storagePrefix: flutterStoragePrefix, - ); -} diff --git a/packages/graphql_flutter/lib/src/widgets/cache_provider.dart b/packages/graphql_flutter/lib/src/widgets/cache_provider.dart index 70d3c7b02..7df342fe9 100644 --- a/packages/graphql_flutter/lib/src/widgets/cache_provider.dart +++ b/packages/graphql_flutter/lib/src/widgets/cache_provider.dart @@ -33,7 +33,7 @@ class _CacheProviderState extends State client = GraphQLProvider.of(context).value; assert(client != null); - client.cache?.restore(); + // client.cache?.restore(); super.didChangeDependencies(); } @@ -45,6 +45,7 @@ class _CacheProviderState extends State WidgetsBinding.instance.removeObserver(this); } +/* @override void didChangeAppLifecycleState(AppLifecycleState state) { assert(client != null); @@ -69,6 +70,7 @@ class _CacheProviderState extends State break; } } +*/ @override Widget build(BuildContext context) => widget.child; diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index d8649b9a3..45406f600 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -9,7 +9,8 @@ authors: - Michael Joseph Rosenthal homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^3.0.1-beta.2 + graphql: #^3.0.1-beta.2 + path: ../graphql gql_exec: ^0.2.2 flutter: sdk: flutter From 673e1ba58f4586f4444ce042ad5ca18160758d8a Mon Sep 17 00:00:00 2001 From: micimize Date: Tue, 19 May 2020 12:28:54 -0500 Subject: [PATCH 021/118] example build struff --- .gitignore | 2 ++ examples/flutter_bloc/lib/main.dart | 4 +--- .../ios/Flutter/flutter_export_environment.sh | 14 -------------- .../example/ios/Runner.xcodeproj/project.pbxproj | 16 +--------------- .../xcshareddata/WorkspaceSettings.xcsettings | 8 -------- 5 files changed, 4 insertions(+), 40 deletions(-) delete mode 100755 packages/graphql_flutter/example/ios/Flutter/flutter_export_environment.sh delete mode 100644 packages/graphql_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/.gitignore b/.gitignore index 055f3cfeb..daf62a9a3 100644 --- a/.gitignore +++ b/.gitignore @@ -112,6 +112,8 @@ unlinked_spec.ds **/ios/.generated/ **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/.last_build_id **/ios/Flutter/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig **/ios/Flutter/app.flx diff --git a/examples/flutter_bloc/lib/main.dart b/examples/flutter_bloc/lib/main.dart index c40a2163e..3b101e839 100644 --- a/examples/flutter_bloc/lib/main.dart +++ b/examples/flutter_bloc/lib/main.dart @@ -53,9 +53,7 @@ class MyApp extends StatelessWidget { final Link _link = _authLink.concat(_httpLink); return GraphQLClient( - cache: OptimisticCache( - dataIdFromObject: typenameDataIdFromObject, - ), + cache: GraphQLCache(), link: _link, ); } diff --git a/packages/graphql_flutter/example/ios/Flutter/flutter_export_environment.sh b/packages/graphql_flutter/example/ios/Flutter/flutter_export_environment.sh deleted file mode 100755 index 2720650c7..000000000 --- a/packages/graphql_flutter/example/ios/Flutter/flutter_export_environment.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/mjr/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/mjr/Documents/code/libraries/dart/graphql-flutter/packages/graphql_flutter/example" -export "FLUTTER_TARGET=lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "SYMROOT=${SOURCE_ROOT}/../build/ios" -export "OTHER_LDFLAGS=$(inherited) -framework Flutter" -export "FLUTTER_FRAMEWORK_DIR=/Users/mjr/flutter/bin/cache/artifacts/engine/ios" -export "FLUTTER_BUILD_NAME=1.0.0" -export "FLUTTER_BUILD_NUMBER=1" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=false" -export "TREE_SHAKE_ICONS=false" diff --git a/packages/graphql_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/graphql_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 9976c2b0b..ad353e3d3 100644 --- a/packages/graphql_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/graphql_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,11 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 85BD965404134B0319457AD8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A9CD12FDAA2BE6D4AEA7ACE /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -30,8 +26,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -42,7 +36,6 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 4A9CD12FDAA2BE6D4AEA7ACE /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -50,7 +43,6 @@ 9235FC581E2F702C430A0573 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -65,8 +57,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 85BD965404134B0319457AD8 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -85,9 +75,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -233,7 +221,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -322,7 +310,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -376,7 +363,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/packages/graphql_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/graphql_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index 949b67898..000000000 --- a/packages/graphql_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - BuildSystemType - Original - - From 1062912fb624b9bf06bf4a269c64900e04e7d620 Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 20 May 2020 10:47:37 -0500 Subject: [PATCH 022/118] refactor(graphql): simplify exceptions and make them gql_link based --- packages/graphql/lib/client.dart | 2 +- .../lib/src/core/observable_query.dart | 4 +- .../graphql/lib/src/core/query_manager.dart | 11 +++-- .../graphql/lib/src/core/query_result.dart | 2 +- ...eration_exception.dart => exceptions.dart} | 44 +++++++++++++---- .../lib/src/exceptions/_base_exceptions.dart | 48 ------------------- .../lib/src/exceptions/exceptions.dart | 2 - 7 files changed, 47 insertions(+), 66 deletions(-) rename packages/graphql/lib/src/{exceptions/operation_exception.dart => exceptions.dart} (53%) delete mode 100644 packages/graphql/lib/src/exceptions/_base_exceptions.dart delete mode 100644 packages/graphql/lib/src/exceptions/exceptions.dart diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index 1cfd993dd..38dbc14e2 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -4,7 +4,7 @@ export 'package:graphql/src/cache/cache.dart'; export 'package:graphql/src/core/query_manager.dart'; export 'package:graphql/src/core/query_options.dart'; export 'package:graphql/src/core/query_result.dart'; -export 'package:graphql/src/exceptions/exceptions.dart'; +export 'package:graphql/src/exceptions.dart'; export 'package:graphql/src/graphql_client.dart'; export 'package:graphql/src/links/links.dart'; diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 79481ff08..ea570fc0e 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:graphql/src/exceptions/exceptions.dart'; +import 'package:graphql/src/exceptions.dart'; import 'package:meta/meta.dart'; import 'package:graphql/src/core/query_manager.dart'; @@ -185,7 +185,7 @@ class ObservableQuery { latestResult.exception = coalesceErrors( exception: latestResult.exception, graphqlErrors: fetchMoreResult.exception.graphqlErrors, - clientException: fetchMoreResult.exception.clientException, + linkException: fetchMoreResult.exception.linkException, ); queryManager.addQueryResult( diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index cda45609a..46d469d56 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -9,7 +9,7 @@ import 'package:graphql/src/cache/cache.dart'; import 'package:graphql/src/core/observable_query.dart'; import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/query_result.dart'; -import 'package:graphql/src/exceptions/exceptions.dart'; +import 'package:graphql/src/exceptions.dart'; import 'package:graphql/src/scheduler/scheduler.dart'; class QueryManager { @@ -130,13 +130,15 @@ class QueryManager { ); } catch (failure) { // TODO: handle Link exceptions + // TODO can we model this transformation as a link // we set the source to indicate where the source of failure queryResult ??= QueryResult(source: QueryResultSource.Network); queryResult.exception = coalesceErrors( exception: queryResult.exception, - clientException: translateFailure(failure), + linkException: + failure is LinkException ? failure : UnknownException(failure), ); } @@ -190,7 +192,7 @@ class QueryManager { queryResult = QueryResult( source: QueryResultSource.Cache, exception: OperationException( - clientException: CacheMissException( + linkException: CacheMissException( 'Could not find that request in the cache. (FetchPolicy.cacheOnly)', cacheKey, ), @@ -201,7 +203,8 @@ class QueryManager { } catch (failure) { queryResult.exception = coalesceErrors( exception: queryResult.exception, - clientException: translateFailure(failure), + linkException: + failure is LinkException ? failure : UnknownException(failure), ); } diff --git a/packages/graphql/lib/src/core/query_result.dart b/packages/graphql/lib/src/core/query_result.dart index c77a1817c..2ca747883 100644 --- a/packages/graphql/lib/src/core/query_result.dart +++ b/packages/graphql/lib/src/core/query_result.dart @@ -1,6 +1,6 @@ import 'dart:async' show FutureOr; -import 'package:graphql/src/exceptions/exceptions.dart'; +import 'package:graphql/src/exceptions.dart'; /// The source of the result data contained /// diff --git a/packages/graphql/lib/src/exceptions/operation_exception.dart b/packages/graphql/lib/src/exceptions.dart similarity index 53% rename from packages/graphql/lib/src/exceptions/operation_exception.dart rename to packages/graphql/lib/src/exceptions.dart index ca9a84e70..ac58a973b 100644 --- a/packages/graphql/lib/src/exceptions/operation_exception.dart +++ b/packages/graphql/lib/src/exceptions.dart @@ -1,5 +1,33 @@ -import 'package:gql_exec/gql_exec.dart'; -import 'package:graphql/src/exceptions/_base_exceptions.dart'; +import 'package:meta/meta.dart'; + +import 'package:gql_link/gql_link.dart' show LinkException; +import 'package:gql_exec/gql_exec.dart' show GraphQLError; + +export 'package:gql_exec/gql_exec.dart' show GraphQLError; + +/// A failure to find a response from the cache when cacheOnly=true +@immutable +class CacheMissException extends LinkException { + CacheMissException(this.message, this.missingKey) : super(null); + + final String message; + final String missingKey; +} + +// +// end cache exceptions +// + +/// Exception occurring when an unhandled, non-link exception +/// is thrown during execution +@immutable +class UnknownException extends LinkException { + String get message => 'Unhandled Client-Side Exception: $originalException'; + + const UnknownException( + dynamic originalException, + ) : super(originalException); +} class OperationException implements Exception { /// Any graphql errors returned from the operation @@ -7,17 +35,17 @@ class OperationException implements Exception { // generalize to include cache error, etc /// Errors encountered during execution such as network or cache errors - ClientException clientException; + LinkException linkException; OperationException({ - this.clientException, + this.linkException, Iterable graphqlErrors = const [], }) : this.graphqlErrors = graphqlErrors.toList(); void addError(GraphQLError error) => graphqlErrors.add(error); String toString() => [ - if (clientException != null) 'ClientException: ${clientException}', + if (linkException != null) 'LinkException: ${linkException}', if (graphqlErrors.isNotEmpty) 'GraphQL Errors:', ...graphqlErrors.map((e) => e.toString()), ].join('\n'); @@ -30,14 +58,14 @@ class OperationException implements Exception { /// NOTE: NULL returns expected OperationException coalesceErrors({ List graphqlErrors, - ClientException clientException, + LinkException linkException, OperationException exception, }) { if (exception != null || - clientException != null || + linkException != null || (graphqlErrors != null && graphqlErrors.isNotEmpty)) { return OperationException( - clientException: clientException ?? exception?.clientException, + linkException: linkException ?? exception?.linkException, graphqlErrors: [ if (graphqlErrors != null) ...graphqlErrors, if (exception?.graphqlErrors != null) ...exception.graphqlErrors diff --git a/packages/graphql/lib/src/exceptions/_base_exceptions.dart b/packages/graphql/lib/src/exceptions/_base_exceptions.dart deleted file mode 100644 index 7f490ce04..000000000 --- a/packages/graphql/lib/src/exceptions/_base_exceptions.dart +++ /dev/null @@ -1,48 +0,0 @@ -abstract class ClientException implements Exception { - String get message; -} - -// -// Cache exceptions -// - -abstract class ClientCacheException implements ClientException {} - -/// A failure during the cache's entity normalization processes -class NormalizationException implements ClientCacheException { - NormalizationException(this.message, this.overflowError, this.value); - - StackOverflowError overflowError; - String message; - Object value; -} - -/// A failure to find a key in the cache when cacheOnly=true -class CacheMissException implements ClientCacheException { - CacheMissException(this.message, this.missingKey); - - String message; - String missingKey; -} - -// -// end cache exceptions -// - -class UnhandledFailureWrapper implements ClientException { - String get message => 'Unhandled Failure $failure'; - - covariant Object failure; - - UnhandledFailureWrapper(this.failure); - - String toString() => message; -} - -ClientException translateFailure(dynamic failure) { - if (failure is ClientException) { - return failure; - } - - return UnhandledFailureWrapper(failure); -} diff --git a/packages/graphql/lib/src/exceptions/exceptions.dart b/packages/graphql/lib/src/exceptions/exceptions.dart deleted file mode 100644 index 8875df2b8..000000000 --- a/packages/graphql/lib/src/exceptions/exceptions.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'package:graphql/src/exceptions/_base_exceptions.dart'; -export 'package:graphql/src/exceptions/operation_exception.dart'; From dcbe127260443458c4275e11bb91479ba632b12b Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 20 May 2020 11:00:47 -0500 Subject: [PATCH 023/118] refactor(graphql): QueryResult loading -> isLoading, optimistic -> isOptimistic --- .../lib/blocs/repos/my_repos_bloc.dart | 5 +-- .../graphql/lib/src/core/query_result.dart | 45 ++++++++++++------- .../test/widgets/query_test.dart | 2 +- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/examples/flutter_bloc/lib/blocs/repos/my_repos_bloc.dart b/examples/flutter_bloc/lib/blocs/repos/my_repos_bloc.dart index d0263389f..9b5d83fc9 100644 --- a/examples/flutter_bloc/lib/blocs/repos/my_repos_bloc.dart +++ b/examples/flutter_bloc/lib/blocs/repos/my_repos_bloc.dart @@ -107,13 +107,12 @@ class MyGithubReposBloc extends Bloc { return; } - var mutatedRepo = - extractRepositoryData(queryResults.data) as LazyCacheMap; + var mutatedRepo = extractRepositoryData(queryResults.data); final notloadingRepo = Repo( id: repo.id, name: repo.name, - viewerHasStarred: mutatedRepo.data['viewerHasStarred'], + viewerHasStarred: mutatedRepo['viewerHasStarred'], isLoading: false, ); diff --git a/packages/graphql/lib/src/core/query_result.dart b/packages/graphql/lib/src/core/query_result.dart index 2ca747883..3f31a3144 100644 --- a/packages/graphql/lib/src/core/query_result.dart +++ b/packages/graphql/lib/src/core/query_result.dart @@ -1,24 +1,38 @@ import 'dart:async' show FutureOr; +import 'package:graphql/client.dart'; import 'package:graphql/src/exceptions.dart'; /// The source of the result data contained /// -/// Loading: No data has been specified from any source -/// Cache: A result has been eagerly resolved from the cache -/// OptimisticResults: An optimistic result has been specified -/// (may include eager results from the cache) -/// Network: The query has been resolved on the network +/// * [Loading]: No data has been specified from any source +/// * [Cache]: A result has been eagerly resolved from the cache +/// * [OptimisticResult]: An optimistic result has been specified +/// May include eager results from the cache. +/// * [Network]: The query has been resolved on the network /// -/// Both Optimistic and Cache sources are considered "Eager" results +/// Both [OptimisticResult] and [Cache] sources are considered "Eager" results. enum QueryResultSource { + /// No data has been specified from any source Loading, + + /// A result has been eagerly resolved from the cache Cache, + + /// An optimistic result has been specified. + /// May include eager results from the cache OptimisticResult, + + /// The query has been resolved on the network Network, } -final eagerSources = { +extension on QueryResultSource { + /// Whether this result source is considered "eager" (is [Cache] or [OptimisticResult]) + bool get isEager => _eagerSources.contains(this); +} + +final _eagerSources = { QueryResultSource.Cache, QueryResultSource.OptimisticResult }; @@ -46,19 +60,20 @@ class QueryResult { /// Will be set when encountering an error during any execution attempt QueryResultSource source; - /// List or Map - dynamic data; + /// Response data + Map data; OperationException exception; - /// Whether data has been specified from either the cache or network) - bool get loading => source == QueryResultSource.Loading; + /// Whether [data] has yet to be specified from either the cache or network + bool get isLoading => source == QueryResultSource.Loading; - /// Whether an optimistic result has been specified - /// (may include eager results from the cache) - bool get optimistic => source == QueryResultSource.OptimisticResult; + /// Whether an optimistic result has been specified. + /// + /// May include eager results from the cache. + bool get isOptimistic => source == QueryResultSource.OptimisticResult; - /// Whether the response includes an exception + /// Whether the response includes an [exception] bool get hasException => (exception != null); } diff --git a/packages/graphql_flutter/test/widgets/query_test.dart b/packages/graphql_flutter/test/widgets/query_test.dart index 0545d8aee..8126a5673 100644 --- a/packages/graphql_flutter/test/widgets/query_test.dart +++ b/packages/graphql_flutter/test/widgets/query_test.dart @@ -90,7 +90,7 @@ void main() { ); client = ValueNotifier( GraphQLClient( - cache: GraphQLCache(storagePrefix: 'test'), + cache: GraphQLCache(store: HiveStore()), link: httpLink, ), ); From 80309a1035a37b688c6036427c76fb9a81c4ee9b Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 20 May 2020 11:09:37 -0500 Subject: [PATCH 024/118] refactor(graphql): QueryResult.loading() and optimistic({ data }) constructors --- .../graphql/lib/src/core/query_result.dart | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/graphql/lib/src/core/query_result.dart b/packages/graphql/lib/src/core/query_result.dart index 3f31a3144..94bcb2b7d 100644 --- a/packages/graphql/lib/src/core/query_result.dart +++ b/packages/graphql/lib/src/core/query_result.dart @@ -2,55 +2,59 @@ import 'dart:async' show FutureOr; import 'package:graphql/client.dart'; import 'package:graphql/src/exceptions.dart'; +import 'package:meta/meta.dart'; /// The source of the result data contained /// -/// * [Loading]: No data has been specified from any source -/// * [Cache]: A result has been eagerly resolved from the cache -/// * [OptimisticResult]: An optimistic result has been specified +/// * [loading]: No data has been specified from any source +/// * [cache]: A result has been eagerly resolved from the cache +/// * [optimisticResult]: An optimistic result has been specified /// May include eager results from the cache. -/// * [Network]: The query has been resolved on the network +/// * [network]: The query has been resolved on the network /// -/// Both [OptimisticResult] and [Cache] sources are considered "Eager" results. +/// Both [optimisticResult] and [cache] sources are considered "Eager" results. enum QueryResultSource { /// No data has been specified from any source - Loading, + loading, /// A result has been eagerly resolved from the cache - Cache, + cache, /// An optimistic result has been specified. /// May include eager results from the cache - OptimisticResult, + optimisticResult, /// The query has been resolved on the network - Network, + network, } extension on QueryResultSource { - /// Whether this result source is considered "eager" (is [Cache] or [OptimisticResult]) + /// Whether this result source is considered "eager" (is [cache] or [optimisticResult]) bool get isEager => _eagerSources.contains(this); } final _eagerSources = { - QueryResultSource.Cache, - QueryResultSource.OptimisticResult + QueryResultSource.cache, + QueryResultSource.optimisticResult }; class QueryResult { QueryResult({ this.data, this.exception, - bool loading, - bool optimistic, - QueryResultSource source, - }) : timestamp = DateTime.now(), - this.source = source ?? - ((loading == true) - ? QueryResultSource.Loading - : (optimistic == true) - ? QueryResultSource.OptimisticResult - : null); + @required this.source, + }) : timestamp = DateTime.now(); + + factory QueryResult.loading() => + QueryResult(source: QueryResultSource.loading); + + factory QueryResult.optimistic({ + Map data, + }) => + QueryResult( + data: data, + source: QueryResultSource.optimisticResult, + ); DateTime timestamp; @@ -66,12 +70,12 @@ class QueryResult { OperationException exception; /// Whether [data] has yet to be specified from either the cache or network - bool get isLoading => source == QueryResultSource.Loading; + bool get isLoading => source == QueryResultSource.loading; /// Whether an optimistic result has been specified. /// /// May include eager results from the cache. - bool get isOptimistic => source == QueryResultSource.OptimisticResult; + bool get isOptimistic => source == QueryResultSource.optimisticResult; /// Whether the response includes an [exception] bool get hasException => (exception != null); @@ -82,10 +86,10 @@ class MultiSourceResult { this.eagerResult, this.networkResult, }) : assert( - eagerResult.source != QueryResultSource.Network, + eagerResult.source != QueryResultSource.network, 'An eager result cannot be gotten from the network', ) { - eagerResult ??= QueryResult(loading: true); + eagerResult ??= QueryResult.loading(); } QueryResult eagerResult; From 2b447a02b3d25ca6398ac02033aa1c7d156be73c Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 20 May 2020 11:23:43 -0500 Subject: [PATCH 025/118] fix(graphql): keep deprecated QueryResult api and mark it as such --- examples/flutter_bloc/lib/extended_bloc.dart | 4 +- .../graphql/lib/src/core/query_manager.dart | 20 +++++----- .../graphql/lib/src/core/query_options.dart | 6 +-- .../graphql/lib/src/core/query_result.dart | 39 ++++++++++++++++++- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/examples/flutter_bloc/lib/extended_bloc.dart b/examples/flutter_bloc/lib/extended_bloc.dart index be18515d2..f36840e12 100644 --- a/examples/flutter_bloc/lib/extended_bloc.dart +++ b/examples/flutter_bloc/lib/extended_bloc.dart @@ -139,8 +139,8 @@ class _ExtendedBlocState extends State { } String parseOperationException(OperationException error) { - if (error.clientException != null) { - final exception = error.clientException; + if (error.linkException != null) { + final exception = error.linkException; if (exception is NetworkException) { return 'Failed to connect to ${exception.uri}'; diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 46d469d56..9534e4c63 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -94,7 +94,7 @@ class QueryManager { return MultiSourceResult( eagerResult: eagerResult, networkResult: - (shouldStopAtCache(options.fetchPolicy) && !eagerResult.loading) + (shouldStopAtCache(options.fetchPolicy) && !eagerResult.isLoading) ? null : _resolveQueryOnNetwork(request, queryId, options), ); @@ -126,14 +126,14 @@ class QueryManager { queryResult = mapFetchResultToQueryResult( response, options, - source: QueryResultSource.Network, + source: QueryResultSource.network, ); } catch (failure) { // TODO: handle Link exceptions // TODO can we model this transformation as a link // we set the source to indicate where the source of failure - queryResult ??= QueryResult(source: QueryResultSource.Network); + queryResult ??= QueryResult(source: QueryResultSource.network); queryResult.exception = coalesceErrors( exception: queryResult.exception, @@ -163,7 +163,7 @@ class QueryManager { ) { final String cacheKey = options.toKey(); - QueryResult queryResult = QueryResult(loading: true); + QueryResult queryResult = QueryResult.loading(); try { if (options.optimisticResult != null) { @@ -177,20 +177,20 @@ class QueryManager { // if we haven't already resolved results optimistically, // we attempt to resolve the from the cache if (shouldRespondEagerlyFromCache(options.fetchPolicy) && - !queryResult.optimistic) { + !queryResult.isOptimistic) { final dynamic data = cache.readQuery(request, optimistic: false); // we only push an eager query with data if (data != null) { queryResult = QueryResult( data: data, - source: QueryResultSource.Cache, + source: QueryResultSource.cache, ); } if (options.fetchPolicy == FetchPolicy.cacheOnly && - queryResult.loading) { + queryResult.isLoading) { queryResult = QueryResult( - source: QueryResultSource.Cache, + source: QueryResultSource.cache, exception: OperationException( linkException: CacheMissException( 'Could not find that request in the cache. (FetchPolicy.cacheOnly)', @@ -267,7 +267,7 @@ class QueryManager { request, optimistic: true, ), - source: QueryResultSource.OptimisticResult, + source: QueryResultSource.optimisticResult, ); return queryResult; } @@ -295,7 +295,7 @@ class QueryManager { Response(data: cachedData), query.options, // TODO maybe entirely wrong - source: QueryResultSource.Cache, + source: QueryResultSource.cache, ), ); } diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index a4e332a91..76d547a11 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -187,7 +187,7 @@ class MutationCallbacks { OnData get onCompleted { if (options.onCompleted != null) { return (QueryResult result) { - if (!result.loading && !result.optimistic) { + if (!result.isLoading && !result.isOptimistic) { return options.onCompleted(result.data); } }; @@ -198,7 +198,7 @@ class MutationCallbacks { OnData get onError { if (options.onError != null) { return (QueryResult result) { - if (!result.loading && + if (!result.isLoading && result.hasException && options.errorPolicy != ErrorPolicy.ignore) { return options.onError(result.exception); @@ -240,7 +240,7 @@ class MutationCallbacks { // wrap update logic to handle optimism void updateOnData(QueryResult result) { - if (result.optimistic) { + if (result.isOptimistic) { return optimisticUpdate(result); } else { return widgetUpdate(cache, result); diff --git a/packages/graphql/lib/src/core/query_result.dart b/packages/graphql/lib/src/core/query_result.dart index 94bcb2b7d..8d4bea75f 100644 --- a/packages/graphql/lib/src/core/query_result.dart +++ b/packages/graphql/lib/src/core/query_result.dart @@ -31,6 +31,26 @@ enum QueryResultSource { extension on QueryResultSource { /// Whether this result source is considered "eager" (is [cache] or [optimisticResult]) bool get isEager => _eagerSources.contains(this); + + /// No data has been specified from any source + @Deprecated( + 'Use `QueryResultSource.loading` instead. Will be removed in 5.0.0') + QueryResultSource get Loading => QueryResultSource.loading; + + /// A result has been eagerly resolved from the cache + @Deprecated('Use `QueryResultSource.cache` instead. Will be removed in 5.0.0') + QueryResultSource get Cache => QueryResultSource.cache; + + /// An optimistic result has been specified. + /// May include eager results from the cache + @Deprecated( + 'Use `QueryResultSource.optimisticResult` instead. Will be removed in 5.0.0') + QueryResultSource get OptimisticResult => QueryResultSource.optimisticResult; + + /// The query has been resolved on the network + @Deprecated( + 'Use `QueryResultSource.network` instead. Will be removed in 5.0.0') + QueryResultSource get Network => QueryResultSource.network; } final _eagerSources = { @@ -45,8 +65,13 @@ class QueryResult { @required this.source, }) : timestamp = DateTime.now(); - factory QueryResult.loading() => - QueryResult(source: QueryResultSource.loading); + factory QueryResult.loading({ + Map data, + }) => + QueryResult( + data: data, + source: QueryResultSource.loading, + ); factory QueryResult.optimistic({ Map data, @@ -72,11 +97,21 @@ class QueryResult { /// Whether [data] has yet to be specified from either the cache or network bool get isLoading => source == QueryResultSource.loading; + /// Whether [data] has yet to be specified from either the cache or network + @Deprecated('Use `isLoading` instead. Will be removed in 5.0.0') + bool get loading => isLoading; + /// Whether an optimistic result has been specified. /// /// May include eager results from the cache. bool get isOptimistic => source == QueryResultSource.optimisticResult; + /// Whether an optimistic result has been specified. + /// + /// May include eager results from the cache. + @Deprecated('Use `isOptimistic` instead. Will be removed in 5.0.0') + bool get optimistic => isOptimistic; + /// Whether the response includes an [exception] bool get hasException => (exception != null); } From 8ab684094db94be0f8711804c5a5d343e839e7b2 Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 20 May 2020 11:50:39 -0500 Subject: [PATCH 026/118] refactor(graphql): recover NetworkException and handle unknown exceptions better --- .../lib/src/core/observable_query.dart | 5 +- .../graphql/lib/src/core/query_manager.dart | 8 +- packages/graphql/lib/src/exceptions.dart | 80 +------------------ .../lib/src/exceptions/exceptions.dart | 16 ++++ .../lib/src/exceptions/exceptions_next.dart | 78 ++++++++++++++++++ .../graphql/lib/src/exceptions/network.dart | 35 ++++++++ .../lib/src/exceptions/network_io.dart | 24 ++++++ 7 files changed, 161 insertions(+), 85 deletions(-) create mode 100644 packages/graphql/lib/src/exceptions/exceptions.dart create mode 100644 packages/graphql/lib/src/exceptions/exceptions_next.dart create mode 100644 packages/graphql/lib/src/exceptions/network.dart create mode 100644 packages/graphql/lib/src/exceptions/network_io.dart diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index ea570fc0e..a3131fadc 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -154,10 +154,7 @@ class ObservableQuery { ); // stream old results with a loading indicator - addResult(QueryResult( - data: latestResult.data, - loading: true, - )); + addResult(QueryResult.loading(data: latestResult.data)); QueryResult fetchMoreResult = await queryManager.query(combinedOptions); diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 9534e4c63..e785c2084 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:gql_exec/gql_exec.dart'; -import 'package:gql_link/gql_link.dart'; +import 'package:gql_link/gql_link.dart' show Link; import 'package:graphql/src/cache/cache.dart'; import 'package:graphql/src/core/observable_query.dart'; @@ -137,8 +137,7 @@ class QueryManager { queryResult.exception = coalesceErrors( exception: queryResult.exception, - linkException: - failure is LinkException ? failure : UnknownException(failure), + linkException: translateFailure(failure), ); } @@ -203,8 +202,7 @@ class QueryManager { } catch (failure) { queryResult.exception = coalesceErrors( exception: queryResult.exception, - linkException: - failure is LinkException ? failure : UnknownException(failure), + linkException: translateFailure(failure), ); } diff --git a/packages/graphql/lib/src/exceptions.dart b/packages/graphql/lib/src/exceptions.dart index ac58a973b..26392f14d 100644 --- a/packages/graphql/lib/src/exceptions.dart +++ b/packages/graphql/lib/src/exceptions.dart @@ -1,76 +1,4 @@ -import 'package:meta/meta.dart'; - -import 'package:gql_link/gql_link.dart' show LinkException; -import 'package:gql_exec/gql_exec.dart' show GraphQLError; - -export 'package:gql_exec/gql_exec.dart' show GraphQLError; - -/// A failure to find a response from the cache when cacheOnly=true -@immutable -class CacheMissException extends LinkException { - CacheMissException(this.message, this.missingKey) : super(null); - - final String message; - final String missingKey; -} - -// -// end cache exceptions -// - -/// Exception occurring when an unhandled, non-link exception -/// is thrown during execution -@immutable -class UnknownException extends LinkException { - String get message => 'Unhandled Client-Side Exception: $originalException'; - - const UnknownException( - dynamic originalException, - ) : super(originalException); -} - -class OperationException implements Exception { - /// Any graphql errors returned from the operation - List graphqlErrors = []; - - // generalize to include cache error, etc - /// Errors encountered during execution such as network or cache errors - LinkException linkException; - - OperationException({ - this.linkException, - Iterable graphqlErrors = const [], - }) : this.graphqlErrors = graphqlErrors.toList(); - - void addError(GraphQLError error) => graphqlErrors.add(error); - - String toString() => [ - if (linkException != null) 'LinkException: ${linkException}', - if (graphqlErrors.isNotEmpty) 'GraphQL Errors:', - ...graphqlErrors.map((e) => e.toString()), - ].join('\n'); -} - -/// `(graphqlErrors?, exception?) => exception?` -/// -/// merges both optional graphqlErrors and an optional container -/// into a single optional container -/// NOTE: NULL returns expected -OperationException coalesceErrors({ - List graphqlErrors, - LinkException linkException, - OperationException exception, -}) { - if (exception != null || - linkException != null || - (graphqlErrors != null && graphqlErrors.isNotEmpty)) { - return OperationException( - linkException: linkException ?? exception?.linkException, - graphqlErrors: [ - if (graphqlErrors != null) ...graphqlErrors, - if (exception?.graphqlErrors != null) ...exception.graphqlErrors - ], - ); - } - return null; -} +/// Once `gql_link` has robust http and socket exception handling, +/// this will be replaced with `./exceptions/exceptions_next.dart` +/// and the rest of `./exceptions/` will be deleted +export './exceptions/exceptions.dart'; diff --git a/packages/graphql/lib/src/exceptions/exceptions.dart b/packages/graphql/lib/src/exceptions/exceptions.dart new file mode 100644 index 000000000..f7e9c6d88 --- /dev/null +++ b/packages/graphql/lib/src/exceptions/exceptions.dart @@ -0,0 +1,16 @@ +import 'package:gql_link/gql_link.dart' show LinkException; +import 'package:graphql/src/exceptions/exceptions_next.dart' + show UnknownException; + +export 'package:graphql/src/exceptions/exceptions_next.dart'; + +import 'package:graphql/src/exceptions/network.dart' + if (dart.library.io) 'package:graphql/src/exceptions/network_io.dart' + as network; + +LinkException translateFailure(dynamic failure) { + if (failure is LinkException) { + return failure; + } + return network.translateFailure(failure) ?? UnknownException(failure); +} diff --git a/packages/graphql/lib/src/exceptions/exceptions_next.dart b/packages/graphql/lib/src/exceptions/exceptions_next.dart new file mode 100644 index 000000000..33a91c6fd --- /dev/null +++ b/packages/graphql/lib/src/exceptions/exceptions_next.dart @@ -0,0 +1,78 @@ +/// Once `gql_link` has robust http and socket exception handling, +/// these should be the only exceptions we need +import 'package:meta/meta.dart'; + +import 'package:gql_link/gql_link.dart' show LinkException; +import 'package:gql_exec/gql_exec.dart' show GraphQLError; + +export 'package:gql_exec/gql_exec.dart' show GraphQLError; + +/// A failure to find a response from the cache when cacheOnly=true +@immutable +class CacheMissException extends LinkException { + CacheMissException(this.message, this.missingKey) : super(null); + + final String message; + final String missingKey; +} + +// +// end cache exceptions +// + +/// Exception occurring when an unhandled, non-link exception +/// is thrown during execution +@immutable +class UnknownException extends LinkException { + String get message => 'Unhandled Client-Side Exception: $originalException'; + + const UnknownException( + dynamic originalException, + ) : super(originalException); +} + +class OperationException implements Exception { + /// Any graphql errors returned from the operation + List graphqlErrors = []; + + // generalize to include cache error, etc + /// Errors encountered during execution such as network or cache errors + LinkException linkException; + + OperationException({ + this.linkException, + Iterable graphqlErrors = const [], + }) : this.graphqlErrors = graphqlErrors.toList(); + + void addError(GraphQLError error) => graphqlErrors.add(error); + + String toString() => [ + if (linkException != null) 'LinkException: ${linkException}', + if (graphqlErrors.isNotEmpty) 'GraphQL Errors:', + ...graphqlErrors.map((e) => e.toString()), + ].join('\n'); +} + +/// `(graphqlErrors?, exception?) => exception?` +/// +/// merges both optional graphqlErrors and an optional container +/// into a single optional container +/// NOTE: NULL returns expected +OperationException coalesceErrors({ + List graphqlErrors, + LinkException linkException, + OperationException exception, +}) { + if (exception != null || + linkException != null || + (graphqlErrors != null && graphqlErrors.isNotEmpty)) { + return OperationException( + linkException: linkException ?? exception?.linkException, + graphqlErrors: [ + if (graphqlErrors != null) ...graphqlErrors, + if (exception?.graphqlErrors != null) ...exception.graphqlErrors + ], + ); + } + return null; +} diff --git a/packages/graphql/lib/src/exceptions/network.dart b/packages/graphql/lib/src/exceptions/network.dart new file mode 100644 index 000000000..ba33959f5 --- /dev/null +++ b/packages/graphql/lib/src/exceptions/network.dart @@ -0,0 +1,35 @@ +import 'package:meta/meta.dart'; +import 'package:http/http.dart' as http show ClientException; + +import 'package:gql_link/gql_link.dart' show LinkException; + +/// Exception occurring when there is a network-level error +class NetworkException extends LinkException { + NetworkException({ + dynamic originalException, + @required this.message, + @required this.uri, + }) : super(originalException); + + final String message; + final Uri uri; + + String toString() => + 'Failed to connect to $uri: ${message ?? originalException}'; +} + +/// We wrap [base.translateFailure] to handle io-specific network errors. +/// +/// Once `gql_link` has robust http and socket exception handling, +/// this and `./network.dart` can be removed and `./exceptions_next.dart` +/// will be all that is necessary +NetworkException translateFailure(dynamic failure) { + if (failure is http.ClientException) { + return NetworkException( + originalException: failure, + message: failure.message, + uri: failure.uri, + ); + } + return null; +} diff --git a/packages/graphql/lib/src/exceptions/network_io.dart b/packages/graphql/lib/src/exceptions/network_io.dart new file mode 100644 index 000000000..40f5a5ee9 --- /dev/null +++ b/packages/graphql/lib/src/exceptions/network_io.dart @@ -0,0 +1,24 @@ +import 'dart:io' as io show SocketException; + +import './network.dart' as base; +export './network.dart' show NetworkException; + +/// We wrap [base.translateFailure] to handle io-specific network errors. +/// +/// Once `gql_link` has robust http and socket exception handling, +/// this and `./unhandled.dart` can be removed and `./exceptions_next.dart` +/// will be all that is necessary +base.NetworkException translateFailure(dynamic failure) { + if (failure is io.SocketException) { + return base.NetworkException( + originalException: failure, + message: failure.message, + uri: Uri( + scheme: 'http', + host: failure.address?.host, + port: failure.port, + ), + ); + } + return base.translateFailure(failure); +} From 84cba43f9a7255b0125464014ee3c40e9b71d2ad Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 21 May 2020 13:48:49 -0500 Subject: [PATCH 027/118] feat: cache now flags itself for broadcasting --- .../src/cache/_normalizing_data_proxy.dart | 58 ++++++++++++------- packages/graphql/lib/src/cache/cache.dart | 24 ++++++++ .../graphql/lib/src/cache/data_proxy.dart | 15 +++-- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart index c85b7df5f..a8a93f880 100644 --- a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart @@ -30,6 +30,10 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { @protected bool get returnPartialData => false; + /// Flag used to request a (re)broadcast from the [QueryManager] + @protected + bool broadcastRequested; + /// Optional `dataIdFromObject` function to pass through to [normalize] DataIdResolver dataIdFromObject; @@ -83,18 +87,23 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { ); void writeQuery( - Request request, + Request request, { Map data, - ) => - normalize( - writer: (dataId, value) => writeNormalized(dataId, value), - query: request.operation.document, - operationName: request.operation.operationName, - variables: request.variables, - data: data, - typePolicies: typePolicies, - dataIdFromObject: dataIdFromObject, - ); + bool broadcast = true, + }) { + normalize( + writer: (dataId, value) => writeNormalized(dataId, value), + query: request.operation.document, + operationName: request.operation.operationName, + variables: request.variables, + data: data, + typePolicies: typePolicies, + dataIdFromObject: dataIdFromObject, + ); + if (broadcast ?? true) { + broadcastRequested = true; + } + } void writeFragment({ @required DocumentNode fragment, @@ -102,15 +111,20 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { @required Map data, String fragmentName, Map variables, - }) => - normalizeFragment( - writer: (dataId, value) => writeNormalized(dataId, value), - fragment: fragment, - idFields: idFields, - data: data, - fragmentName: fragmentName, - variables: variables, - typePolicies: typePolicies, - dataIdFromObject: dataIdFromObject, - ); + bool broadcast = true, + }) { + normalizeFragment( + writer: (dataId, value) => writeNormalized(dataId, value), + fragment: fragment, + idFields: idFields, + data: data, + fragmentName: fragmentName, + variables: variables, + typePolicies: typePolicies, + dataIdFromObject: dataIdFromObject, + ); + if (broadcast ?? true) { + broadcastRequested = true; + } + } } diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index 02a64547e..04b52942d 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -26,6 +26,26 @@ class GraphQLCache extends NormalizingDataProxy { final Map typePolicies; final DataIdResolver dataIdFromObject; + /// tracks the number of ongoing transactions to prevent + /// rebroadcasts until they are completed + @protected + int inflightOptimisticTransactions = 0; + + /// Whether a cache operation has requested a broadcast and it is safe to do. + /// + /// The caller must [claimExectution] to clear the [broadcastRequested] flag. + /// + /// This is not meant to be called outside of the [QueryManager] + bool shouldBroadcast({bool claimExecution = false}) { + if (inflightOptimisticTransactions == 0 && this.broadcastRequested) { + if (claimExecution) { + this.broadcastRequested = false; + } + return true; + } + return false; + } + /// List of patches recorded through [recordOptimisticTransaction] /// /// They are applied in ascending order, @@ -115,10 +135,13 @@ class GraphQLCache extends NormalizingDataProxy { CacheTransaction transaction, String addId, ) { + inflightOptimisticTransactions += 1; final _proxy = transaction(OptimisticProxy(this)) as OptimisticProxy; if (_safeToAdd(addId)) { optimisticPatches.add(_proxy.asPatch(addId)); + broadcastRequested = broadcastRequested || _proxy.broadcastRequested; } + inflightOptimisticTransactions -= 1; } /// Remove a given patch from the list @@ -132,5 +155,6 @@ class GraphQLCache extends NormalizingDataProxy { optimisticPatches.removeWhere( (patch) => patch.id == removeId || _parentPatchId(patch.id) == removeId, ); + broadcastRequested = true; } } diff --git a/packages/graphql/lib/src/cache/data_proxy.dart b/packages/graphql/lib/src/cache/data_proxy.dart index df21ea4ca..d24a74489 100644 --- a/packages/graphql/lib/src/cache/data_proxy.dart +++ b/packages/graphql/lib/src/cache/data_proxy.dart @@ -25,15 +25,21 @@ abstract class GraphQLDataProxy { bool optimistic, }); - /// Writes a GraphQL query to the root query id. + /// Writes a GraphQL query to the root query id, + /// then [broadcast] changes to watchers unless `broadcast: false` /// - /// [normalize] the given `data` into the cache using graphql metadata from `request` + /// [normalize] the given [data] into the cache using graphql metadata from [request] /// /// Conceptually, this can be thought of as providing a manual execution result - /// in the form of `data` - void writeQuery(Request request, Map data); + /// in the form of [data] + void writeQuery( + Request request, { + Map data, + bool broadcast, + }); /// Writes a GraphQL fragment to any arbitrary id. + /// then [broadcast] changes to watchers unless `broadcast: false` /// /// If there is more than one fragment in the provided document /// then a `fragmentName` must be provided to select the correct fragment. @@ -43,5 +49,6 @@ abstract class GraphQLDataProxy { @required Map data, String fragmentName, Map variables, + bool broadcast, }); } From 814ccb33264a36e7898e5817d5be563366fcea2b Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 21 May 2020 13:49:42 -0500 Subject: [PATCH 028/118] feat: client.fetchMore utility for leveraging the fetch more logic results without using ObservableQuery --- packages/graphql/README.md | 6 +- packages/graphql/example/bin/main.dart | 7 +- packages/graphql/lib/client.dart | 2 + packages/graphql/lib/src/core/fetch_more.dart | 98 ++++++++++++++ .../lib/src/core/observable_query.dart | 86 ++++--------- packages/graphql/lib/src/core/policies.dart | 121 ++++++++++++++++++ .../graphql/lib/src/core/query_manager.dart | 64 +++++---- .../graphql/lib/src/core/query_options.dart | 83 +----------- .../lib/src/exceptions/exceptions_next.dart | 6 +- packages/graphql/lib/src/graphql_client.dart | 70 +++------- .../graphql/lib/src/scheduler/scheduler.dart | 1 + .../lib/src/widgets/query.dart | 4 +- 12 files changed, 314 insertions(+), 234 deletions(-) create mode 100644 packages/graphql/lib/src/core/fetch_more.dart create mode 100644 packages/graphql/lib/src/core/policies.dart diff --git a/packages/graphql/README.md b/packages/graphql/README.md index 5991e53f1..9d7e5e68a 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -18,7 +18,7 @@ First, depend on this package: ```yaml dependencies: - graphql: ^3.0.0 + graphql: ^4.0.0-rc1 ``` And then import it inside your dart code: @@ -45,13 +45,13 @@ dev_dependencies: To connect to a GraphQL Server, we first need to create a `GraphQLClient`. A `GraphQLClient` requires both a `cache` and a `link` to be initialized. -In our example below, we will be using the Github Public API. In our example below, we are going to use `HttpLink` which we will concatenate with `AuthLink` so as to attach our github access token. For the cache, we are going to use `GraphQLCache`. +In our example below, we will be using the Github Public API. we are going to use `HttpLink` which we will concatenate with `AuthLink` so as to attach our github access token. For the cache, we are going to use `GraphQLCache`. ```dart // ... final HttpLink _httpLink = HttpLink( - uri: 'https://api.github.com/graphql', + 'https://api.github.com/graphql', ); final AuthLink _authLink = AuthLink( diff --git a/packages/graphql/example/bin/main.dart b/packages/graphql/example/bin/main.dart index 5a95b214a..8e4ff8919 100644 --- a/packages/graphql/example/bin/main.dart +++ b/packages/graphql/example/bin/main.dart @@ -16,6 +16,11 @@ ArgResults argResults; // client - create a graphql client GraphQLClient client() { + /// `graphql/client.dart` leverages the [gql_link][1] interface, + /// re-exporting `HttpLink`, `WebsocketLink`, `ErrorLink`, and `DedupeLink`, + /// in addition to the links we define ourselves (`AuthLink`) + /// + /// [1]: https://pub.dev/packages/gql_link final Link _link = HttpLink( 'https://api.github.com/graphql', defaultHeaders: { @@ -37,7 +42,7 @@ void query() async { final QueryOptions options = QueryOptions( document: gql(readRepositories), - variables: { + variables: { 'nRepositories': nRepositories, }, ); diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index 38dbc14e2..6d9d31fda 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -3,7 +3,9 @@ library graphql; export 'package:graphql/src/cache/cache.dart'; export 'package:graphql/src/core/query_manager.dart'; export 'package:graphql/src/core/query_options.dart'; +export 'package:graphql/src/core/fetch_more.dart' show FetchMoreOptions; export 'package:graphql/src/core/query_result.dart'; +export 'package:graphql/src/core/policies.dart'; export 'package:graphql/src/exceptions.dart'; export 'package:graphql/src/graphql_client.dart'; diff --git a/packages/graphql/lib/src/core/fetch_more.dart b/packages/graphql/lib/src/core/fetch_more.dart new file mode 100644 index 000000000..717292b9a --- /dev/null +++ b/packages/graphql/lib/src/core/fetch_more.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:gql/ast.dart'; +import 'package:graphql/client.dart'; +import 'package:meta/meta.dart'; + +import 'package:graphql/src/core/query_manager.dart'; +import 'package:graphql/src/core/query_options.dart'; +import 'package:graphql/src/core/query_result.dart'; +import 'package:graphql/src/core/policies.dart'; + +/// options for fetchmore operations +class FetchMoreOptions { + FetchMoreOptions({ + @required this.document, + this.variables = const {}, + @required this.updateQuery, + }) : assert(updateQuery != null); + + DocumentNode document; + + final Map variables; + + /// Strategy for merging the fetchMore result data + /// with the result data already in the cache + UpdateQuery updateQuery; +} + +/// Fetch more results and then merge them with [previousResult] +/// according to [FetchMoreOptions.updateQuery] +/// +/// Will add results if [ObservableQuery.queryId] is supplied, +/// and broadcast any cache changes +/// +/// This is the **Internal Implementation**, +/// used by [ObservableQuery] and [GraphQLCLient.fetchMore] +Future fetchMoreImplementation( + FetchMoreOptions fetchMoreOptions, { + @required QueryOptions originalOptions, + @required QueryManager queryManager, + @required QueryResult previousResult, + String queryId, +}) async { + // fetch more and udpate + assert(fetchMoreOptions.updateQuery != null); + + final document = (fetchMoreOptions.document ?? originalOptions.document); + + assert( + document != null, + 'Either fetchMoreOptions.document ' + 'or the previous QueryOptions must be supplied!', + ); + + final combinedOptions = QueryOptions( + fetchPolicy: FetchPolicy.noCache, + errorPolicy: originalOptions.errorPolicy, + document: document, + variables: { + ...originalOptions.variables, + ...fetchMoreOptions.variables, + }, + ); + + QueryResult fetchMoreResult = await queryManager.query(combinedOptions); + + try { + // combine the query with the new query, using the function provided by the user + fetchMoreResult.data = fetchMoreOptions.updateQuery( + previousResult.data, + fetchMoreResult.data, + ); + assert(fetchMoreResult.data != null, 'updateQuery result cannot be null'); + // will add to a stream with `queryId` and rebroadcast if appropriate + queryManager.addQueryResult( + originalOptions.asRequest, + queryId, + fetchMoreResult, + writeToCache: originalOptions.fetchPolicy != FetchPolicy.noCache, + ); + } catch (error) { + if (fetchMoreResult.hasException) { + // because the updateQuery failure might have been because of these errors, + // we just add them to the old errors + previousResult.exception = coalesceErrors( + exception: previousResult.exception, + graphqlErrors: fetchMoreResult.exception.graphqlErrors, + linkException: fetchMoreResult.exception.linkException, + ); + return previousResult; + } else { + // TODO merge results OperationException + rethrow; + } + } + + return fetchMoreResult; +} diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index a3131fadc..02235aa57 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -1,11 +1,11 @@ import 'dart:async'; - -import 'package:graphql/src/exceptions.dart'; import 'package:meta/meta.dart'; import 'package:graphql/src/core/query_manager.dart'; import 'package:graphql/src/core/query_options.dart'; +import 'package:graphql/src/core/fetch_more.dart'; import 'package:graphql/src/core/query_result.dart'; +import 'package:graphql/src/core/policies.dart'; import 'package:graphql/src/scheduler/scheduler.dart'; typedef OnData = void Function(QueryResult result); @@ -22,6 +22,11 @@ enum QueryLifecycle { CLOSED } +/// An Observable/Stream-based API returned from `watchQuery` for use in reactive programming +/// +/// Modelled closely after [Apollo's ObservableQuery][apollo_oq] +/// +/// [apollo_oq]: https://www.apollographql.com/docs/react/v3.0-beta/api/core/ObservableQuery/ class ObservableQuery { ObservableQuery({ @required this.queryManager, @@ -39,6 +44,7 @@ class ObservableQuery { // set to true when eagerly fetched to prevent back-to-back queries bool _latestWasEagerlyFetched = false; + /// The identity of this query within the [QueryManager] final String queryId; final QueryManager queryManager; @@ -136,67 +142,21 @@ class ObservableQuery { return allResults; } - /// fetch more results and then merge them according to the updateQuery method. - /// the results will then be added to to stream for the widget to re-build - void fetchMore(FetchMoreOptions fetchMoreOptions) async { - // fetch more and udpate + /// fetch more results and then merge them with the [latestResult] + /// according to [FetchMoreOptions.updateQuery]. + /// The results will then be added to to stream for the widget to re-build + Future fetchMore(FetchMoreOptions fetchMoreOptions) async { assert(fetchMoreOptions.updateQuery != null); - final combinedOptions = QueryOptions( - fetchPolicy: FetchPolicy.noCache, - errorPolicy: options.errorPolicy, - document: fetchMoreOptions.document ?? options.document, - context: options.context, - variables: { - ...options.variables, - ...fetchMoreOptions.variables, - }, - ); - - // stream old results with a loading indicator addResult(QueryResult.loading(data: latestResult.data)); - QueryResult fetchMoreResult = await queryManager.query(combinedOptions); - - final request = options.asRequest; - - try { - // combine the query with the new query, using the function provided by the user - fetchMoreResult.data = fetchMoreOptions.updateQuery( - latestResult.data, - fetchMoreResult.data, - ); - assert(fetchMoreResult.data != null, 'updateQuery result cannot be null'); - - // stream the new results and rebuild - queryManager.addQueryResult( - request, - queryId, - fetchMoreResult, - writeToCache: true, - ); - } catch (error) { - if (fetchMoreResult.hasException) { - // because the updateQuery failure might have been because of these errors, - // we just add them to the old errors - latestResult.exception = coalesceErrors( - exception: latestResult.exception, - graphqlErrors: fetchMoreResult.exception.graphqlErrors, - linkException: fetchMoreResult.exception.linkException, - ); - - queryManager.addQueryResult( - request, - queryId, - latestResult, - writeToCache: true, - ); - return; - } else { - // TODO merge results OperationException - rethrow; - } - } + return fetchMoreImplementation( + fetchMoreOptions, + originalOptions: options, + queryManager: queryManager, + previousResult: latestResult, + queryId: queryId, + ); } /// add a result to the stream, @@ -213,7 +173,7 @@ class ObservableQuery { result.source ??= latestResult.source; } - if (lifecycle == QueryLifecycle.PENDING && result.optimistic != true) { + if (lifecycle == QueryLifecycle.PENDING && !result.isOptimistic) { lifecycle = QueryLifecycle.COMPLETED; } @@ -232,14 +192,12 @@ class ObservableQuery { StreamSubscription subscription; subscription = stream.listen((QueryResult result) async { - if (!result.loading) { + if (!result.isLoading) { for (final callback in callbacks) { await callback(result); } - queryManager.rebroadcastQueries(); - - if (!result.optimistic) { + if (!result.isOptimistic) { await subscription.cancel(); _onDataSubscriptions.remove(subscription); diff --git a/packages/graphql/lib/src/core/policies.dart b/packages/graphql/lib/src/core/policies.dart new file mode 100644 index 000000000..de90f3917 --- /dev/null +++ b/packages/graphql/lib/src/core/policies.dart @@ -0,0 +1,121 @@ +/// [FetchPolicy] determines where the client may return a result from. The options are: +/// - cacheFirst (default): return result from cache. Only fetch from network if cached result is not available. +/// - cacheAndNetwork: return result from cache first (if it exists), then return network result once it's available. +/// - cacheOnly: return result from cache if available, fail otherwise. +/// - noCache: return result from network, fail if network call doesn't succeed, don't save to cache. +/// - networkOnly: return result from network, fail if network call doesn't succeed, save to cache. +enum FetchPolicy { + cacheFirst, + cacheAndNetwork, + cacheOnly, + noCache, + networkOnly, +} + +// TODO investigate the relationship between optimistic results +// and policy in flutter +bool shouldRespondEagerlyFromCache(FetchPolicy fetchPolicy) => + fetchPolicy == FetchPolicy.cacheFirst || + fetchPolicy == FetchPolicy.cacheAndNetwork || + fetchPolicy == FetchPolicy.cacheOnly; + +bool shouldStopAtCache(FetchPolicy fetchPolicy) => + fetchPolicy == FetchPolicy.cacheFirst || + fetchPolicy == FetchPolicy.cacheOnly; + +/// [ErrorPolicy] determines the level of events for errors in the execution result. The options are: +/// - none (default): Any GraphQL Errors are treated the same as network errors and any data is ignored from the response. +/// - ignore: Ignore allows you to read any data that is returned alongside GraphQL Errors, +/// but doesn't save the errors or report them to your UI. +/// - all: Using the all policy is the best way to notify your users of potential issues while still showing as much data as possible from your server. +/// It saves both data and errors into the Apollo Cache so your UI can use them. + +enum ErrorPolicy { + none, + ignore, + all, +} + +/// Container for supplying a [fetch] and [error] policy. +/// +/// If either are `null`, the appropriate policy will be selected from [DefaultPolicies] +class Policies { + /// Specifies the [FetchPolicy] to be used. + FetchPolicy fetch; + + /// Specifies the [ErrorPolicy] to be used. + ErrorPolicy error; + + Policies({ + this.fetch, + this.error, + }); + + Policies.safe( + this.fetch, + this.error, + ) : assert(fetch != null, 'fetch policy must be specified'), + assert(error != null, 'error policy must be specified'); + + Policies withOverrides([Policies overrides]) => Policies.safe( + overrides?.fetch ?? fetch, + overrides?.error ?? error, + ); + + operator ==(Object other) => + other is Policies && fetch == other.fetch && error == other.error; +} + +/// The default [Policies] to set for each client action +class DefaultPolicies { + /// The default [Policies] for watchQuery. + /// Defaults to + /// ``` + /// Policies( + /// FetchPolicy.cacheAndNetwork, + /// ErrorPolicy.none, + /// ) + /// ``` + Policies watchQuery; + + /// The default [Policies] for query. + /// Defaults to + /// ``` + /// Policies( + /// FetchPolicy.cacheFirst, + /// ErrorPolicy.none, + /// ) + /// ``` + Policies query; + + /// The default [Policies] for mutate. + /// Defaults to + /// ``` + /// Policies( + /// FetchPolicy.networkOnly, + /// ErrorPolicy.none, + /// ) + /// ``` + Policies mutate; + DefaultPolicies({ + Policies watchQuery, + Policies query, + Policies mutate, + }) : this.watchQuery = _watchQueryDefaults.withOverrides(watchQuery), + this.query = _queryDefaults.withOverrides(query), + this.mutate = _mutateDefaults.withOverrides(mutate); + + static final _watchQueryDefaults = Policies.safe( + FetchPolicy.cacheAndNetwork, + ErrorPolicy.none, + ); + + static final _queryDefaults = Policies.safe( + FetchPolicy.cacheFirst, + ErrorPolicy.none, + ); + static final _mutateDefaults = Policies.safe( + FetchPolicy.networkOnly, + ErrorPolicy.none, + ); +} diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index e785c2084..98a8c04a2 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -9,6 +9,7 @@ import 'package:graphql/src/cache/cache.dart'; import 'package:graphql/src/core/observable_query.dart'; import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/query_result.dart'; +import 'package:graphql/src/core/policies.dart'; import 'package:graphql/src/exceptions.dart'; import 'package:graphql/src/scheduler/scheduler.dart'; @@ -27,6 +28,8 @@ class QueryManager { QueryScheduler scheduler; int idCounter = 1; + + /// [ObservableQuery] registry Map queries = {}; ObservableQuery watchQuery(WatchQueryOptions options) { @@ -120,7 +123,7 @@ class QueryManager { // save the data from response to the cache if (response.data != null && options.fetchPolicy != FetchPolicy.noCache) { - cache.writeQuery(request, response.data); + cache.writeQuery(request, data: response.data); } queryResult = mapFetchResultToQueryResult( @@ -143,6 +146,7 @@ class QueryManager { // cleanup optimistic results cache.removeOptimisticPatch(queryId); + if (options.fetchPolicy != FetchPolicy.noCache) { // normalize results if previously written queryResult.data = cache.readQuery(request); @@ -160,15 +164,13 @@ class QueryManager { String queryId, BaseOptions options, ) { - final String cacheKey = options.toKey(); - QueryResult queryResult = QueryResult.loading(); try { if (options.optimisticResult != null) { queryResult = _getOptimisticQueryResult( request, - cacheKey: cacheKey, + queryId: queryId, optimisticResult: options.optimisticResult, ); } @@ -192,8 +194,8 @@ class QueryManager { source: QueryResultSource.cache, exception: OperationException( linkException: CacheMissException( - 'Could not find that request in the cache. (FetchPolicy.cacheOnly)', - cacheKey, + 'Could not resolve the given request against the cache. (FetchPolicy.cacheOnly)', + request, ), ), ); @@ -209,11 +211,12 @@ class QueryManager { // If not a regular eager cache resolution, // will either be loading, or optimistic. // - // if there's an optimistic result, we add it regardless of fetchPolicy + // if there's an optimistic result, we add it regardless of fetchPolicy. // This is undefined-ish behavior/edge case, but still better than just // ignoring a provided optimisticResult. // Would probably be better to add it ignoring the cache in such cases addQueryResult(request, queryId, queryResult); + return queryResult; } @@ -231,6 +234,7 @@ class QueryManager { } /// Add a result to the [ObservableQuery] specified by `queryId`, if it exists + /// Will [maybeRebroadcastQueries] if the cache has flagged the need to /// /// Queries are registered via [setQuery] and [watchQuery] void addQueryResult( @@ -239,26 +243,32 @@ class QueryManager { QueryResult queryResult, { bool writeToCache = false, }) { - final ObservableQuery observableQuery = getQuery(queryId); if (writeToCache) { cache.writeQuery( request, - queryResult.data, + data: queryResult.data, ); } + final ObservableQuery observableQuery = getQuery(queryId); + if (observableQuery != null && !observableQuery.controller.isClosed) { observableQuery.addResult(queryResult); } + + maybeRebroadcastQueries(exclude: observableQuery); } /// Create an optimstic result for the query specified by `queryId`, if it exists QueryResult _getOptimisticQueryResult( Request request, { - @required String cacheKey, + @required String queryId, @required Object optimisticResult, }) { - cache.writeQuery(request, optimisticResult); + cache.recordOptimisticTransaction( + (proxy) => proxy..writeQuery(request, data: optimisticResult), + queryId, + ); final QueryResult queryResult = QueryResult( data: cache.readQuery( @@ -267,26 +277,27 @@ class QueryManager { ), source: QueryResultSource.optimisticResult, ); - return queryResult; - } - /// Remove the optimistic patch for `cacheKey`, if any - void cleanupOptimisticResults(String cacheKey) { - cache.removeOptimisticPatch(cacheKey); + return queryResult; } - /// Push changed data from cache to query streams + /// Push changed data from cache to query streams. + /// [exclude] is used to skip a query if it was recently executed + /// (normally the query that caused the rebroadcast) /// - /// rebroadcast queries inherit `optimistic` - /// from the triggering state-change - // TODO ^ no longer true. I would like to recoup the entity-wise - // TODO cache state optimistic awareness - void rebroadcastQueries() { + /// Returns whether a broadcast was executed, which depends on the state of the cache. + /// If there are multiple in-flight cache updates, we wait until they all complete + bool maybeRebroadcastQueries({ObservableQuery exclude}) { + final shouldBroadast = cache.shouldBroadcast(claimExecution: true); + if (!shouldBroadast) { + return false; + } for (ObservableQuery query in queries.values) { - if (query.isRebroadcastSafe) { - // TODO use queryId everywhere or nah - final dynamic cachedData = - cache.readQuery(query.options.asRequest, optimistic: true); + if (query != exclude && query.isRebroadcastSafe) { + final dynamic cachedData = cache.readQuery( + query.options.asRequest, + optimistic: true, + ); if (cachedData != null) { query.addResult( mapFetchResultToQueryResult( @@ -299,6 +310,7 @@ class QueryManager { } } } + return true; } void setQuery(ObservableQuery observableQuery) { diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index 76d547a11..cb693307b 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -8,71 +8,7 @@ import 'package:graphql/client.dart'; import 'package:graphql/internal.dart'; import 'package:graphql/src/core/raw_operation_data.dart'; import 'package:graphql/src/utilities/helpers.dart'; - -/// [FetchPolicy] determines where the client may return a result from. The options are: -/// - cacheFirst (default): return result from cache. Only fetch from network if cached result is not available. -/// - cacheAndNetwork: return result from cache first (if it exists), then return network result once it's available. -/// - cacheOnly: return result from cache if available, fail otherwise. -/// - noCache: return result from network, fail if network call doesn't succeed, don't save to cache. -/// - networkOnly: return result from network, fail if network call doesn't succeed, save to cache. -enum FetchPolicy { - cacheFirst, - cacheAndNetwork, - cacheOnly, - noCache, - networkOnly, -} - -// TODO investigate the relationship between optimistic results -// and policy in flutter -bool shouldRespondEagerlyFromCache(FetchPolicy fetchPolicy) => - fetchPolicy == FetchPolicy.cacheFirst || - fetchPolicy == FetchPolicy.cacheAndNetwork || - fetchPolicy == FetchPolicy.cacheOnly; - -bool shouldStopAtCache(FetchPolicy fetchPolicy) => - fetchPolicy == FetchPolicy.cacheFirst || - fetchPolicy == FetchPolicy.cacheOnly; - -/// [ErrorPolicy] determines the level of events for errors in the execution result. The options are: -/// - none (default): Any GraphQL Errors are treated the same as network errors and any data is ignored from the response. -/// - ignore: Ignore allows you to read any data that is returned alongside GraphQL Errors, -/// but doesn't save the errors or report them to your UI. -/// - all: Using the all policy is the best way to notify your users of potential issues while still showing as much data as possible from your server. -/// It saves both data and errors into the Apollo Cache so your UI can use them. - -enum ErrorPolicy { - none, - ignore, - all, -} - -class Policies { - /// Specifies the [FetchPolicy] to be used. - FetchPolicy fetch; - - /// Specifies the [ErrorPolicy] to be used. - ErrorPolicy error; - - Policies({ - this.fetch, - this.error, - }); - - Policies.safe( - this.fetch, - this.error, - ) : assert(fetch != null, 'fetch policy must be specified'), - assert(error != null, 'error policy must be specified'); - - Policies withOverrides([Policies overrides]) => Policies.safe( - overrides?.fetch ?? fetch, - overrides?.error ?? error, - ); - - operator ==(Object other) => - other is Policies && fetch == other.fetch && error == other.error; -} +import 'package:graphql/src/core/policies.dart'; /// Base options. class BaseOptions extends RawOperationData { @@ -329,20 +265,3 @@ typedef dynamic UpdateQuery( dynamic previousResultData, dynamic fetchMoreResultData, ); - -/// options for fetchmore operations -class FetchMoreOptions { - FetchMoreOptions({ - @required this.document, - this.variables = const {}, - @required this.updateQuery, - }) : assert(updateQuery != null); - - DocumentNode document; - - final Map variables; - - /// Strategy for merging the fetchMore result data - /// with the result data already in the cache - UpdateQuery updateQuery; -} diff --git a/packages/graphql/lib/src/exceptions/exceptions_next.dart b/packages/graphql/lib/src/exceptions/exceptions_next.dart index 33a91c6fd..15b15a2ba 100644 --- a/packages/graphql/lib/src/exceptions/exceptions_next.dart +++ b/packages/graphql/lib/src/exceptions/exceptions_next.dart @@ -3,17 +3,17 @@ import 'package:meta/meta.dart'; import 'package:gql_link/gql_link.dart' show LinkException; -import 'package:gql_exec/gql_exec.dart' show GraphQLError; +import 'package:gql_exec/gql_exec.dart' show GraphQLError, Request; export 'package:gql_exec/gql_exec.dart' show GraphQLError; /// A failure to find a response from the cache when cacheOnly=true @immutable class CacheMissException extends LinkException { - CacheMissException(this.message, this.missingKey) : super(null); + CacheMissException(this.message, this.request) : super(null); final String message; - final String missingKey; + final Request request; } // diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 99c78d463..d7aced36f 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -1,3 +1,4 @@ +import 'package:meta/meta.dart'; import 'dart:async'; import 'package:gql_exec/gql_exec.dart'; @@ -7,61 +8,10 @@ import 'package:graphql/src/core/observable_query.dart'; import 'package:graphql/src/core/query_manager.dart'; import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/query_result.dart'; -import 'package:meta/meta.dart'; - -/// The default [Policies] to set for each client action -class DefaultPolicies { - /// The default [Policies] for watchQuery. - /// Defaults to - /// ``` - /// Policies( - /// FetchPolicy.cacheAndNetwork, - /// ErrorPolicy.none, - /// ) - /// ``` - Policies watchQuery; - - /// The default [Policies] for query. - /// Defaults to - /// ``` - /// Policies( - /// FetchPolicy.cacheFirst, - /// ErrorPolicy.none, - /// ) - /// ``` - Policies query; - /// The default [Policies] for mutate. - /// Defaults to - /// ``` - /// Policies( - /// FetchPolicy.networkOnly, - /// ErrorPolicy.none, - /// ) - /// ``` - Policies mutate; - DefaultPolicies({ - Policies watchQuery, - Policies query, - Policies mutate, - }) : this.watchQuery = _watchQueryDefaults.withOverrides(watchQuery), - this.query = _queryDefaults.withOverrides(query), - this.mutate = _mutateDefaults.withOverrides(mutate); +import 'package:graphql/src/core/fetch_more.dart'; - static final _watchQueryDefaults = Policies.safe( - FetchPolicy.cacheAndNetwork, - ErrorPolicy.none, - ); - - static final _queryDefaults = Policies.safe( - FetchPolicy.cacheFirst, - ErrorPolicy.none, - ); - static final _mutateDefaults = Policies.safe( - FetchPolicy.networkOnly, - ErrorPolicy.none, - ); -} +import 'package:graphql/src/core/policies.dart' show DefaultPolicies; /// The link is a [Link] over which GraphQL documents will be resolved into a [Response]. /// The cache is the initial [Cache] to use in the data store. @@ -117,4 +67,18 @@ class GraphQLClient { Stream subscribe(Request request) { return link.request(request); } + + /// Fetch more results and then merge them with the given [previousResult] + /// according to [FetchMoreOptions.updateQuery]. + Future fetchMore( + FetchMoreOptions fetchMoreOptions, { + @required QueryOptions originalOptions, + @required QueryResult previousResult, + }) => + fetchMoreImplementation( + fetchMoreOptions, + originalOptions: originalOptions, + previousResult: previousResult, + queryManager: queryManager, + ); } diff --git a/packages/graphql/lib/src/scheduler/scheduler.dart b/packages/graphql/lib/src/scheduler/scheduler.dart index 1f54e1d95..918ae78f1 100644 --- a/packages/graphql/lib/src/scheduler/scheduler.dart +++ b/packages/graphql/lib/src/scheduler/scheduler.dart @@ -4,6 +4,7 @@ import 'package:graphql/src/core/query_manager.dart'; import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/observable_query.dart'; +/// Handles scheduling polling results for each [ObservableQuery] with a `pollInterval` class QueryScheduler { QueryScheduler({ this.queryManager, diff --git a/packages/graphql_flutter/lib/src/widgets/query.dart b/packages/graphql_flutter/lib/src/widgets/query.dart index acab54040..c96baff21 100644 --- a/packages/graphql_flutter/lib/src/widgets/query.dart +++ b/packages/graphql_flutter/lib/src/widgets/query.dart @@ -74,7 +74,7 @@ class QueryState extends State { final optionsWithOverrides = _options; optionsWithOverrides.policies = client.defaultPolicies.watchQuery - .withOverrides(optionsWithOverrides.policies); + .withOverrides(optionsWithOverrides.policies); if (!observableQuery.options.areEqualTo(optionsWithOverrides)) { _initQuery(); @@ -91,7 +91,7 @@ class QueryState extends State { Widget build(BuildContext context) { return StreamBuilder( key: Key(observableQuery?.options?.toKey()), - initialData: observableQuery?.latestResult ?? QueryResult(loading: true), + initialData: observableQuery?.latestResult ?? QueryResult.loading(), stream: observableQuery.stream, builder: ( BuildContext buildContext, From ca12bfc765908aff00398064f8b29f590ab5fbf4 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 23 May 2020 14:38:33 -0500 Subject: [PATCH 029/118] going to go back to mutable api after all --- .../graphql/lib/src/core/_base_options.dart | 69 +++++ .../graphql/lib/src/core/_data_class.dart | 28 ++ .../lib/src/core/mutation_options.dart | 164 ++++++++++ packages/graphql/lib/src/core/policies.dart | 62 +++- .../graphql/lib/src/core/query_manager.dart | 1 + .../graphql/lib/src/core/query_options.dart | 285 ++++++------------ .../lib/src/core/raw_operation_data.dart | 57 ---- .../lib/src/widgets/_subscription.dart | 108 +++++++ 8 files changed, 521 insertions(+), 253 deletions(-) create mode 100644 packages/graphql/lib/src/core/_base_options.dart create mode 100644 packages/graphql/lib/src/core/_data_class.dart create mode 100644 packages/graphql/lib/src/core/mutation_options.dart delete mode 100644 packages/graphql/lib/src/core/raw_operation_data.dart create mode 100644 packages/graphql_flutter/lib/src/widgets/_subscription.dart diff --git a/packages/graphql/lib/src/core/_base_options.dart b/packages/graphql/lib/src/core/_base_options.dart new file mode 100644 index 000000000..1016a2358 --- /dev/null +++ b/packages/graphql/lib/src/core/_base_options.dart @@ -0,0 +1,69 @@ +import 'package:graphql/src/core/_data_class.dart'; +import 'package:meta/meta.dart'; + +import 'package:gql/ast.dart'; +import 'package:gql_exec/gql_exec.dart'; + +import 'package:graphql/client.dart'; +import 'package:graphql/src/core/policies.dart'; + +/// Base options. +@immutable +class BaseOptions extends DataClass { + BaseOptions({ + @required this.document, + this.operationName, + this.variables, + Context context, + FetchPolicy fetchPolicy, + ErrorPolicy errorPolicy, + this.optimisticResult, + }) : policies = Policies(fetch: fetchPolicy, error: errorPolicy), + context = context ?? Context(); + + /// Document containing at least one [OperationDefinitionNode] + final DocumentNode document; + + /// Name of the executable definition + /// + /// Must be specified if [document] contains more than one [OperationDefinitionNode] + final String operationName; + + /// A map going from variable name to variable value, where the variables are used + /// within the GraphQL query. + final Map variables; + + /// An optimistic result to eagerly add to the operation stream + final Object optimisticResult; + + /// Specifies the [Policies] to be used during execution. + final Policies policies; + + FetchPolicy get fetchPolicy => policies.fetch; + + ErrorPolicy get errorPolicy => policies.error; + + /// Context to be passed to link execution chain. + final Context context; + + // TODO consider inverting this relationship + /// Resolve these options into a request + Request get asRequest => Request( + operation: Operation( + document: document, + operationName: operationName, + ), + variables: variables, + context: context, + ); + + @override + List get properties => [ + document, + operationName, + variables, + optimisticResult, + policies, + context, + ]; +} diff --git a/packages/graphql/lib/src/core/_data_class.dart b/packages/graphql/lib/src/core/_data_class.dart new file mode 100644 index 000000000..0f12ddac8 --- /dev/null +++ b/packages/graphql/lib/src/core/_data_class.dart @@ -0,0 +1,28 @@ +import 'package:meta/meta.dart'; +import "package:collection/collection.dart"; + +/// Similar to Equatable using the same approach as `gql`'s data classes +@immutable +abstract class DataClass { + const DataClass(); + + @protected + List get properties; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DataClass && + runtimeType == other.runtimeType && + const ListEquality( + DeepCollectionEquality(), + ).equals( + other.properties, + properties, + )); + + @override + int get hashCode => const ListEquality( + DeepCollectionEquality(), + ).hash(properties); +} diff --git a/packages/graphql/lib/src/core/mutation_options.dart b/packages/graphql/lib/src/core/mutation_options.dart new file mode 100644 index 000000000..ba81cb8cb --- /dev/null +++ b/packages/graphql/lib/src/core/mutation_options.dart @@ -0,0 +1,164 @@ +import 'package:graphql/src/cache/cache.dart'; +import 'package:graphql/src/core/_base_options.dart'; +import 'package:graphql/src/core/observable_query.dart'; +import 'package:meta/meta.dart'; + +import 'package:gql/ast.dart'; +import 'package:gql_exec/gql_exec.dart'; + +import 'package:graphql/src/exceptions.dart'; +import 'package:graphql/src/core/query_result.dart'; +import 'package:graphql/src/utilities/helpers.dart'; +import 'package:graphql/src/core/policies.dart'; + +typedef OnMutationCompleted = void Function(dynamic data); +typedef OnMutationUpdate = void Function( + GraphQLDataProxy cache, + QueryResult result, +); +typedef OnError = void Function(OperationException error); + +class MutationOptions extends BaseOptions { + MutationOptions({ + @required DocumentNode document, + String operationName, + Map variables, + FetchPolicy fetchPolicy, + ErrorPolicy errorPolicy, + Context context, + Object optimisticResult, + this.onCompleted, + this.update, + this.onError, + }) : super( + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, + document: document, + operationName: operationName, + variables: variables, + context: context, + optimisticResult: optimisticResult, + ); + + final OnMutationCompleted onCompleted; + final OnMutationUpdate update; + final OnError onError; + + @override + List get properties => + [...super.properties, onCompleted, update, onError]; + + MutationOptions copyWith({ + DocumentNode document, + String operationName, + Map variables, + Context context, + FetchPolicy fetchPolicy, + ErrorPolicy errorPolicy, + Object optimisticResult, + OnMutationCompleted onCompleted, + OnMutationUpdate update, + OnError onError, + }) => + MutationOptions( + document: document ?? this.document, + operationName: operationName ?? this.operationName, + variables: variables ?? this.variables, + context: context ?? this.context, + fetchPolicy: fetchPolicy ?? this.fetchPolicy, + errorPolicy: errorPolicy ?? this.errorPolicy, + optimisticResult: optimisticResult ?? this.optimisticResult, + onCompleted: onCompleted ?? this.onCompleted, + update: update ?? this.update, + onError: onError ?? this.onError, + ); +} + +/// Handles execution of mutation `update`, `onCompleted`, and `onError` callbacks +class MutationCallbackHandler { + final MutationOptions options; + final GraphQLCache cache; + final String queryId; + + MutationCallbackHandler({ + this.options, + this.cache, + this.queryId, + }) : assert(cache != null), + assert(options != null), + assert(queryId != null); + + // callbacks will be called against each result in the stream, + // which should then rebroadcast queries with the appropriate optimism + Iterable get callbacks => + [onCompleted, update, onError].where(notNull); + + // Todo: probably move this to its own class + OnData get onCompleted { + if (options.onCompleted != null) { + return (QueryResult result) { + if (!result.isLoading && !result.isOptimistic) { + return options.onCompleted(result.data); + } + }; + } + return null; + } + + OnData get onError { + if (options.onError != null) { + return (QueryResult result) { + if (!result.isLoading && + result.hasException && + options.errorPolicy != ErrorPolicy.ignore) { + return options.onError(result.exception); + } + }; + } + + return null; + } + + /// The optimistic cache layer id `update` will write to + /// is a "child patch" of the default optimistic patch + /// created by the query manager + String get _patchId => '${queryId}.update'; + + /// apply the user's patch + void _optimisticUpdate(QueryResult result) { + final String patchId = _patchId; + // this is also done in query_manager, but better safe than sorry + cache.recordOptimisticTransaction( + (GraphQLDataProxy cache) { + options.update(cache, result); + return cache; + }, + patchId, + ); + } + + // optimistic patches will be cleaned up by the query_manager + // cleanup is handled by heirarchical optimism - + // as in, because our patch id is prefixed with '${observableQuery.queryId}.', + // it will be discarded along with the observableQuery.queryId patch + // TODO this results in an implicit coupling with the patch id system + OnData get update { + if (options.update != null) { + // dereference all variables that might be needed if the widget is disposed + final OnMutationUpdate widgetUpdate = options.update; + final OnData optimisticUpdate = _optimisticUpdate; + + // wrap update logic to handle optimism + void updateOnData(QueryResult result) { + if (result.isOptimistic) { + return optimisticUpdate(result); + } else { + return widgetUpdate(cache, result); + } + } + + return updateOnData; + } + return null; + } +} diff --git a/packages/graphql/lib/src/core/policies.dart b/packages/graphql/lib/src/core/policies.dart index de90f3917..20d1ad0c7 100644 --- a/packages/graphql/lib/src/core/policies.dart +++ b/packages/graphql/lib/src/core/policies.dart @@ -1,3 +1,6 @@ +import 'package:meta/meta.dart'; +import "package:collection/collection.dart"; + /// [FetchPolicy] determines where the client may return a result from. The options are: /// - cacheFirst (default): return result from cache. Only fetch from network if cached result is not available. /// - cacheAndNetwork: return result from cache first (if it exists), then return network result once it's available. @@ -39,12 +42,13 @@ enum ErrorPolicy { /// Container for supplying a [fetch] and [error] policy. /// /// If either are `null`, the appropriate policy will be selected from [DefaultPolicies] +@immutable class Policies { /// Specifies the [FetchPolicy] to be used. - FetchPolicy fetch; + final FetchPolicy fetch; /// Specifies the [ErrorPolicy] to be used. - ErrorPolicy error; + final ErrorPolicy error; Policies({ this.fetch, @@ -62,11 +66,21 @@ class Policies { overrides?.error ?? error, ); + Policies copyWith({FetchPolicy fetch, ErrorPolicy error}) => + Policies(fetch: fetch, error: error); + operator ==(Object other) => - other is Policies && fetch == other.fetch && error == other.error; + identical(this, other) || + (other is Policies && fetch == other.fetch && error == other.error); + + @override + int get hashCode => const ListEquality( + DeepCollectionEquality(), + ).hash([fetch, error]); } /// The default [Policies] to set for each client action +@immutable class DefaultPolicies { /// The default [Policies] for watchQuery. /// Defaults to @@ -76,7 +90,7 @@ class DefaultPolicies { /// ErrorPolicy.none, /// ) /// ``` - Policies watchQuery; + final Policies watchQuery; /// The default [Policies] for query. /// Defaults to @@ -86,7 +100,7 @@ class DefaultPolicies { /// ErrorPolicy.none, /// ) /// ``` - Policies query; + final Policies query; /// The default [Policies] for mutate. /// Defaults to @@ -96,7 +110,8 @@ class DefaultPolicies { /// ErrorPolicy.none, /// ) /// ``` - Policies mutate; + final Policies mutate; + DefaultPolicies({ Policies watchQuery, Policies query, @@ -118,4 +133,39 @@ class DefaultPolicies { FetchPolicy.networkOnly, ErrorPolicy.none, ); + + DefaultPolicies copyWith({ + Policies watchQuery, + Policies query, + Policies mutate, + }) => + DefaultPolicies( + watchQuery: watchQuery, + query: query, + mutate: mutate, + ); + + List _getChildren() => [ + watchQuery, + query, + mutate, + ]; + + @override + bool operator ==(Object o) => + identical(this, o) || + (o is DefaultPolicies && + const ListEquality( + DeepCollectionEquality(), + ).equals( + o._getChildren(), + _getChildren(), + )); + + @override + int get hashCode => const ListEquality( + DeepCollectionEquality(), + ).hash( + _getChildren(), + ); } diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 98a8c04a2..90bf2bf96 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:graphql/src/core/_base_options.dart'; import 'package:meta/meta.dart'; import 'package:gql_exec/gql_exec.dart'; diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index cb693307b..a7c14cbb7 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -1,4 +1,6 @@ import 'package:graphql/src/cache/cache.dart'; +import 'package:graphql/src/core/_base_options.dart'; +import 'package:graphql/src/core/_data_class.dart'; import 'package:meta/meta.dart'; import 'package:gql/ast.dart'; @@ -6,52 +8,14 @@ import 'package:gql_exec/gql_exec.dart'; import 'package:graphql/client.dart'; import 'package:graphql/internal.dart'; -import 'package:graphql/src/core/raw_operation_data.dart'; import 'package:graphql/src/utilities/helpers.dart'; import 'package:graphql/src/core/policies.dart'; -/// Base options. -class BaseOptions extends RawOperationData { - BaseOptions({ - @required DocumentNode document, - Map variables, - this.policies, - this.context, - this.optimisticResult, - }) : super( - document: document, - variables: variables, - ); - - /// An optimistic result to eagerly add to the operation stream - Object optimisticResult; - - /// Specifies the [Policies] to be used during execution. - Policies policies; - - FetchPolicy get fetchPolicy => policies.fetch; - - ErrorPolicy get errorPolicy => policies.error; - - /// Context to be passed to link execution chain. - Context context; - - // TODO consider inverting this relationship - /// Resolve these options into a request - Request get asRequest => Request( - operation: Operation( - document: document, - operationName: operationName, - ), - variables: variables, - context: context ?? Context(), - ); -} - /// Query options. class QueryOptions extends BaseOptions { QueryOptions({ @required DocumentNode document, + String operationName, Map variables, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, @@ -59,8 +23,10 @@ class QueryOptions extends BaseOptions { this.pollInterval, Context context, }) : super( - policies: Policies(fetch: fetchPolicy, error: errorPolicy), + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, document: document, + operationName: operationName, variables: variables, context: context, optimisticResult: optimisticResult, @@ -68,195 +34,134 @@ class QueryOptions extends BaseOptions { /// The time interval (in milliseconds) on which this query should be /// re-fetched from the server. - int pollInterval; + final int pollInterval; + + @override + List get properties => [...super.properties, pollInterval]; } -typedef OnMutationCompleted = void Function(dynamic data); -typedef OnMutationUpdate = void Function( - GraphQLDataProxy cache, - QueryResult result, -); -typedef OnError = void Function(OperationException error); +extension on QueryOptions { + QueryOptions copyWith({ + DocumentNode document, + String operationName, + Map variables, + Context context, + FetchPolicy fetchPolicy, + ErrorPolicy errorPolicy, + Object optimisticResult, + int pollInterval, + }) => + QueryOptions( + document: document ?? this.document, + operationName: operationName ?? this.operationName, + variables: variables ?? this.variables, + context: context ?? this.context, + fetchPolicy: fetchPolicy ?? this.fetchPolicy, + errorPolicy: errorPolicy ?? this.errorPolicy, + optimisticResult: optimisticResult ?? this.optimisticResult, + pollInterval: pollInterval ?? this.pollInterval, + ); +} -/// Mutation options -class MutationOptions extends BaseOptions { - MutationOptions({ +class SubscriptionOptions extends BaseOptions { + SubscriptionOptions({ @required DocumentNode document, + String operationName, Map variables, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, + Object optimisticResult, Context context, - this.onCompleted, - this.update, - this.onError, }) : super( - policies: Policies(fetch: fetchPolicy, error: errorPolicy), + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, document: document, + operationName: operationName, variables: variables, context: context, + optimisticResult: optimisticResult, ); - OnMutationCompleted onCompleted; - OnMutationUpdate update; - OnError onError; + SubscriptionOptions copyWith({ + DocumentNode document, + String operationName, + Map variables, + Context context, + FetchPolicy fetchPolicy, + ErrorPolicy errorPolicy, + Object optimisticResult, + }) => + SubscriptionOptions( + document: document ?? this.document, + operationName: operationName ?? this.operationName, + variables: variables ?? this.variables, + context: context ?? this.context, + fetchPolicy: fetchPolicy ?? this.fetchPolicy, + errorPolicy: errorPolicy ?? this.errorPolicy, + optimisticResult: optimisticResult ?? this.optimisticResult, + ); } -class MutationCallbacks { - final MutationOptions options; - final GraphQLCache cache; - final String queryId; - - MutationCallbacks({ - this.options, - this.cache, - this.queryId, - }) : assert(cache != null), - assert(options != null), - assert(queryId != null); - - // callbacks will be called against each result in the stream, - // which should then rebroadcast queries with the appropriate optimism - Iterable get callbacks => - [onCompleted, update, onError].where(notNull); - - // Todo: probably move this to its own class - OnData get onCompleted { - if (options.onCompleted != null) { - return (QueryResult result) { - if (!result.isLoading && !result.isOptimistic) { - return options.onCompleted(result.data); - } - }; - } - return null; - } - - OnData get onError { - if (options.onError != null) { - return (QueryResult result) { - if (!result.isLoading && - result.hasException && - options.errorPolicy != ErrorPolicy.ignore) { - return options.onError(result.exception); - } - }; - } - - return null; - } - - /// The optimistic cache layer id `update` will write to - /// is a "child patch" of the default optimistic patch - /// created by the query manager - String get _patchId => '${queryId}.update'; - - /// apply the user's patch - void _optimisticUpdate(QueryResult result) { - final String patchId = _patchId; - // this is also done in query_manager, but better safe than sorry - cache.recordOptimisticTransaction( - (GraphQLDataProxy cache) { - options.update(cache, result); - return cache; - }, - patchId, - ); - } - - // optimistic patches will be cleaned up by the query_manager - // cleanup is handled by heirarchical optimism - - // as in, because our patch id is prefixed with '${observableQuery.queryId}.', - // it will be discarded along with the observableQuery.queryId patch - // TODO this results in an implicit coupling with the patch id system - OnData get update { - if (options.update != null) { - // dereference all variables that might be needed if the widget is disposed - final OnMutationUpdate widgetUpdate = options.update; - final OnData optimisticUpdate = _optimisticUpdate; - - // wrap update logic to handle optimism - void updateOnData(QueryResult result) { - if (result.isOptimistic) { - return optimisticUpdate(result); - } else { - return widgetUpdate(cache, result); - } - } - - return updateOnData; - } - return null; - } -} +/// Mutation options // ObservableQuery options + class WatchQueryOptions extends QueryOptions { WatchQueryOptions({ @required DocumentNode document, + String operationName, Map variables, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, Object optimisticResult, int pollInterval, this.fetchResults = false, - this.eagerlyFetchResults, + bool eagerlyFetchResults, Context context, - }) : super( + }) : eagerlyFetchResults = eagerlyFetchResults ?? fetchResults, + super( document: document, + operationName: operationName, variables: variables, fetchPolicy: fetchPolicy, errorPolicy: errorPolicy, pollInterval: pollInterval, context: context, optimisticResult: optimisticResult, - ) { - this.eagerlyFetchResults ??= fetchResults; - } - - /// Whether or not to fetch result. - bool fetchResults; - bool eagerlyFetchResults; - - /// Checks if the [WatchQueryOptions] in this class are equal to some given options. - bool areEqualTo(WatchQueryOptions otherOptions) { - return !_areDifferentOptions(this, otherOptions); - } - - /// Checks if two options are equal. - bool _areDifferentOptions( - WatchQueryOptions a, - WatchQueryOptions b, - ) { - if (a.document != b.document) { - return true; - } - - if (a.policies != b.policies) { - return true; - } + ); - if (a.pollInterval != b.pollInterval) { - return true; - } + /// Whether or not to fetch results + final bool fetchResults; - if (a.fetchResults != b.fetchResults) { - return true; - } + final bool eagerlyFetchResults; - // compare variables last, because maps take more time - return areDifferentVariables(a.variables, b.variables); - } + @override + List get properties => + [...super.properties, fetchResults, eagerlyFetchResults]; - WatchQueryOptions copy() => WatchQueryOptions( - document: document, - variables: variables, - fetchPolicy: fetchPolicy, - errorPolicy: errorPolicy, - optimisticResult: optimisticResult, - pollInterval: pollInterval, - fetchResults: fetchResults, - eagerlyFetchResults: eagerlyFetchResults, - context: context, + WatchQueryOptions copyWith({ + DocumentNode document, + String operationName, + Map variables, + Context context, + FetchPolicy fetchPolicy, + ErrorPolicy errorPolicy, + Object optimisticResult, + int pollInterval, + bool fetchResults, + bool eagerlyFetchResults, + }) => + WatchQueryOptions( + document: document ?? this.document, + operationName: operationName ?? this.operationName, + variables: variables ?? this.variables, + context: context ?? this.context, + fetchPolicy: fetchPolicy ?? this.fetchPolicy, + errorPolicy: errorPolicy ?? this.errorPolicy, + optimisticResult: optimisticResult ?? this.optimisticResult, + pollInterval: pollInterval ?? this.pollInterval, + fetchResults: fetchResults ?? this.fetchResults, + eagerlyFetchResults: eagerlyFetchResults ?? this.eagerlyFetchResults, ); } diff --git a/packages/graphql/lib/src/core/raw_operation_data.dart b/packages/graphql/lib/src/core/raw_operation_data.dart deleted file mode 100644 index 415e0a756..000000000 --- a/packages/graphql/lib/src/core/raw_operation_data.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'dart:collection' show SplayTreeMap; -import 'dart:convert' show json; - -import 'package:gql/ast.dart'; -import 'package:graphql/src/utilities/get_from_ast.dart'; -import 'package:meta/meta.dart'; - -class RawOperationData { - RawOperationData({ - @required this.document, - Map variables, - String operationName, - }) : _operationName = operationName, - variables = SplayTreeMap.of( - variables ?? const {}, - ); - - /// A GraphQL document that consists of a single query to be sent down to the server. - DocumentNode document; - - /// A map going from variable name to variable value, where the variables are used - /// within the GraphQL query. - Map variables; - - String _operationName; - - /// The last operation name appearing in the contained document. - String get operationName { - _operationName ??= getLastOperationName(document); - return _operationName; - } - - String _documentIdentifier; - - /// The client identifier for this operation, - // TODO remove $document from key? A bit redundant, though that's not the worst thing - String get _identifier { - _documentIdentifier ??= - operationName ?? 'UNNAMED/' + document.hashCode.toString(); - return _documentIdentifier; - } - - String toKey() { - /// SplayTreeMap is always sorted - final String encodedVariables = json.encode( - variables, - toEncodable: (dynamic object) { - // TODO: transparently handle multipart file without introducing package:http - // default toEncodable behavior - return object.toJson(); - }, - ); - - // TODO: document is being depracated, find ways for generating key - return '$document|$encodedVariables|$_identifier'; - } -} diff --git a/packages/graphql_flutter/lib/src/widgets/_subscription.dart b/packages/graphql_flutter/lib/src/widgets/_subscription.dart new file mode 100644 index 000000000..c96baff21 --- /dev/null +++ b/packages/graphql_flutter/lib/src/widgets/_subscription.dart @@ -0,0 +1,108 @@ +import 'package:flutter/widgets.dart'; + +import 'package:graphql/client.dart'; +import 'package:graphql/internal.dart'; + +import 'package:graphql_flutter/src/widgets/graphql_provider.dart'; + +// method to call from widget to fetchmore queries +typedef FetchMore = dynamic Function(FetchMoreOptions options); + +typedef Refetch = Future Function(); + +typedef QueryBuilder = Widget Function( + QueryResult result, { + Refetch refetch, + FetchMore fetchMore, +}); + +/// Builds a [Query] widget based on the a given set of [QueryOptions] +/// that streams [QueryResult]s into the [QueryBuilder]. +class Query extends StatefulWidget { + const Query({ + final Key key, + @required this.options, + @required this.builder, + }) : super(key: key); + + final QueryOptions options; + final QueryBuilder builder; + + @override + QueryState createState() => QueryState(); +} + +class QueryState extends State { + ObservableQuery observableQuery; + GraphQLClient _client; + WatchQueryOptions get _options { + final QueryOptions options = widget.options; + + return WatchQueryOptions( + document: options.document, + variables: options.variables, + fetchPolicy: options.fetchPolicy, + errorPolicy: options.errorPolicy, + pollInterval: options.pollInterval, + fetchResults: true, + context: options.context, + optimisticResult: options.optimisticResult, + ); + } + + void _initQuery() { + observableQuery?.close(); + observableQuery = _client.watchQuery(_options); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final GraphQLClient client = GraphQLProvider.of(context).value; + assert(client != null); + if (client != _client) { + _client = client; + _initQuery(); + } + } + + @override + void didUpdateWidget(Query oldWidget) { + super.didUpdateWidget(oldWidget); + + final GraphQLClient client = GraphQLProvider.of(context).value; + + final optionsWithOverrides = _options; + optionsWithOverrides.policies = client.defaultPolicies.watchQuery + .withOverrides(optionsWithOverrides.policies); + + if (!observableQuery.options.areEqualTo(optionsWithOverrides)) { + _initQuery(); + } + } + + @override + void dispose() { + observableQuery?.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + key: Key(observableQuery?.options?.toKey()), + initialData: observableQuery?.latestResult ?? QueryResult.loading(), + stream: observableQuery.stream, + builder: ( + BuildContext buildContext, + AsyncSnapshot snapshot, + ) { + return widget?.builder( + snapshot.data, + refetch: observableQuery.refetch, + fetchMore: observableQuery.fetchMore, + ); + }, + ); + } +} From 94d8380fa0f143cdb755faee9e1e71bf481b8752 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 23 May 2020 17:13:19 -0500 Subject: [PATCH 030/118] refactor(graphql): generalized MutableDataClass, reorg, delete graphql/internal.dart --- packages/graphql/lib/internal.dart | 3 - .../src/cache/_optimistic_transactions.dart | 2 +- .../graphql/lib/src/core/_base_options.dart | 15 ++- .../graphql/lib/src/core/_data_class.dart | 22 ++-- packages/graphql/lib/src/core/core.dart | 9 ++ packages/graphql/lib/src/core/fetch_more.dart | 18 --- .../lib/src/core/mutation_options.dart | 25 ---- packages/graphql/lib/src/core/policies.dart | 2 +- .../graphql/lib/src/core/query_manager.dart | 5 +- .../graphql/lib/src/core/query_options.dart | 107 +++++------------- packages/graphql/lib/src/graphql_client.dart | 9 +- .../graphql/lib/src/utilities/helpers.dart | 27 ----- packages/graphql/test/cache/cache_data.dart | 1 - .../test/cache/graphql_cache_test.dart | 18 +-- 14 files changed, 70 insertions(+), 193 deletions(-) delete mode 100644 packages/graphql/lib/internal.dart create mode 100644 packages/graphql/lib/src/core/core.dart diff --git a/packages/graphql/lib/internal.dart b/packages/graphql/lib/internal.dart deleted file mode 100644 index d38c2a107..000000000 --- a/packages/graphql/lib/internal.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'package:graphql/src/utilities/helpers.dart'; - -export 'package:graphql/src/core/observable_query.dart'; diff --git a/packages/graphql/lib/src/cache/_optimistic_transactions.dart b/packages/graphql/lib/src/cache/_optimistic_transactions.dart index 7eb552c17..52538c10b 100644 --- a/packages/graphql/lib/src/cache/_optimistic_transactions.dart +++ b/packages/graphql/lib/src/cache/_optimistic_transactions.dart @@ -1,7 +1,7 @@ /// Optimistic proxying and patching classes and typedefs used by `./cache.dart` import 'dart:collection'; -import 'package:graphql/internal.dart'; +import 'package:graphql/src/utilities/helpers.dart'; import 'package:meta/meta.dart'; import 'package:graphql/src/cache/_normalizing_data_proxy.dart'; diff --git a/packages/graphql/lib/src/core/_base_options.dart b/packages/graphql/lib/src/core/_base_options.dart index 1016a2358..73a96345e 100644 --- a/packages/graphql/lib/src/core/_base_options.dart +++ b/packages/graphql/lib/src/core/_base_options.dart @@ -8,8 +8,7 @@ import 'package:graphql/client.dart'; import 'package:graphql/src/core/policies.dart'; /// Base options. -@immutable -class BaseOptions extends DataClass { +abstract class BaseOptions extends MutableDataClass { BaseOptions({ @required this.document, this.operationName, @@ -22,29 +21,29 @@ class BaseOptions extends DataClass { context = context ?? Context(); /// Document containing at least one [OperationDefinitionNode] - final DocumentNode document; + DocumentNode document; /// Name of the executable definition /// /// Must be specified if [document] contains more than one [OperationDefinitionNode] - final String operationName; + String operationName; /// A map going from variable name to variable value, where the variables are used /// within the GraphQL query. - final Map variables; + Map variables; /// An optimistic result to eagerly add to the operation stream - final Object optimisticResult; + Object optimisticResult; /// Specifies the [Policies] to be used during execution. - final Policies policies; + Policies policies; FetchPolicy get fetchPolicy => policies.fetch; ErrorPolicy get errorPolicy => policies.error; /// Context to be passed to link execution chain. - final Context context; + Context context; // TODO consider inverting this relationship /// Resolve these options into a request diff --git a/packages/graphql/lib/src/core/_data_class.dart b/packages/graphql/lib/src/core/_data_class.dart index 0f12ddac8..c7ea40009 100644 --- a/packages/graphql/lib/src/core/_data_class.dart +++ b/packages/graphql/lib/src/core/_data_class.dart @@ -1,28 +1,22 @@ import 'package:meta/meta.dart'; import "package:collection/collection.dart"; -/// Similar to Equatable using the same approach as `gql`'s data classes -@immutable -abstract class DataClass { - const DataClass(); +/// Helper for making mutable data clases +abstract class MutableDataClass { + const MutableDataClass(); + /// identifying properties for the inheriting class @protected List get properties; - @override - bool operator ==(Object other) => + /// [properties] based equality check + bool equal(MutableDataClass other) => identical(this, other) || - (other is DataClass && - runtimeType == other.runtimeType && + (runtimeType == other.runtimeType && const ListEquality( DeepCollectionEquality(), - ).equals( + ).equal( other.properties, properties, )); - - @override - int get hashCode => const ListEquality( - DeepCollectionEquality(), - ).hash(properties); } diff --git a/packages/graphql/lib/src/core/core.dart b/packages/graphql/lib/src/core/core.dart new file mode 100644 index 000000000..a8e64a905 --- /dev/null +++ b/packages/graphql/lib/src/core/core.dart @@ -0,0 +1,9 @@ +export 'package:gql_exec/gql_exec.dart'; +export 'package:gql_link/gql_link.dart'; + +export 'package:graphql/src/core/observable_query.dart'; +export 'package:graphql/src/core/query_manager.dart'; +export 'package:graphql/src/core/query_options.dart'; +export 'package:graphql/src/core/mutation_options.dart'; +export 'package:graphql/src/core/query_result.dart'; +export 'package:graphql/src/core/policies.dart'; diff --git a/packages/graphql/lib/src/core/fetch_more.dart b/packages/graphql/lib/src/core/fetch_more.dart index 717292b9a..60259fdcc 100644 --- a/packages/graphql/lib/src/core/fetch_more.dart +++ b/packages/graphql/lib/src/core/fetch_more.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:gql/ast.dart'; import 'package:graphql/client.dart'; import 'package:meta/meta.dart'; @@ -9,23 +8,6 @@ import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/query_result.dart'; import 'package:graphql/src/core/policies.dart'; -/// options for fetchmore operations -class FetchMoreOptions { - FetchMoreOptions({ - @required this.document, - this.variables = const {}, - @required this.updateQuery, - }) : assert(updateQuery != null); - - DocumentNode document; - - final Map variables; - - /// Strategy for merging the fetchMore result data - /// with the result data already in the cache - UpdateQuery updateQuery; -} - /// Fetch more results and then merge them with [previousResult] /// according to [FetchMoreOptions.updateQuery] /// diff --git a/packages/graphql/lib/src/core/mutation_options.dart b/packages/graphql/lib/src/core/mutation_options.dart index ba81cb8cb..d7ef6c28c 100644 --- a/packages/graphql/lib/src/core/mutation_options.dart +++ b/packages/graphql/lib/src/core/mutation_options.dart @@ -47,31 +47,6 @@ class MutationOptions extends BaseOptions { @override List get properties => [...super.properties, onCompleted, update, onError]; - - MutationOptions copyWith({ - DocumentNode document, - String operationName, - Map variables, - Context context, - FetchPolicy fetchPolicy, - ErrorPolicy errorPolicy, - Object optimisticResult, - OnMutationCompleted onCompleted, - OnMutationUpdate update, - OnError onError, - }) => - MutationOptions( - document: document ?? this.document, - operationName: operationName ?? this.operationName, - variables: variables ?? this.variables, - context: context ?? this.context, - fetchPolicy: fetchPolicy ?? this.fetchPolicy, - errorPolicy: errorPolicy ?? this.errorPolicy, - optimisticResult: optimisticResult ?? this.optimisticResult, - onCompleted: onCompleted ?? this.onCompleted, - update: update ?? this.update, - onError: onError ?? this.onError, - ); } /// Handles execution of mutation `update`, `onCompleted`, and `onError` callbacks diff --git a/packages/graphql/lib/src/core/policies.dart b/packages/graphql/lib/src/core/policies.dart index 20d1ad0c7..53fe53608 100644 --- a/packages/graphql/lib/src/core/policies.dart +++ b/packages/graphql/lib/src/core/policies.dart @@ -157,7 +157,7 @@ class DefaultPolicies { (o is DefaultPolicies && const ListEquality( DeepCollectionEquality(), - ).equals( + ).equal( o._getChildren(), _getChildren(), )); diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 90bf2bf96..21d71f2a3 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:graphql/src/core/_base_options.dart'; import 'package:meta/meta.dart'; import 'package:gql_exec/gql_exec.dart'; @@ -8,6 +7,8 @@ import 'package:gql_link/gql_link.dart' show Link; import 'package:graphql/src/cache/cache.dart'; import 'package:graphql/src/core/observable_query.dart'; +import 'package:graphql/src/core/_base_options.dart'; +import 'package:graphql/src/core/mutation_options.dart'; import 'package:graphql/src/core/query_options.dart'; import 'package:graphql/src/core/query_result.dart'; import 'package:graphql/src/core/policies.dart'; @@ -53,7 +54,7 @@ class QueryManager { // not sure why query id is '0', may be needs improvements // once the mutation has been process successfully, execute callbacks // before returning the results - final mutationCallbacks = MutationCallbacks( + final mutationCallbacks = MutationCallbackHandler( cache: cache, options: options, queryId: '0', diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index a7c14cbb7..1abccc5ab 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -1,14 +1,10 @@ -import 'package:graphql/src/cache/cache.dart'; import 'package:graphql/src/core/_base_options.dart'; -import 'package:graphql/src/core/_data_class.dart'; import 'package:meta/meta.dart'; import 'package:gql/ast.dart'; import 'package:gql_exec/gql_exec.dart'; import 'package:graphql/client.dart'; -import 'package:graphql/internal.dart'; -import 'package:graphql/src/utilities/helpers.dart'; import 'package:graphql/src/core/policies.dart'; /// Query options. @@ -34,35 +30,12 @@ class QueryOptions extends BaseOptions { /// The time interval (in milliseconds) on which this query should be /// re-fetched from the server. - final int pollInterval; + int pollInterval; @override List get properties => [...super.properties, pollInterval]; } -extension on QueryOptions { - QueryOptions copyWith({ - DocumentNode document, - String operationName, - Map variables, - Context context, - FetchPolicy fetchPolicy, - ErrorPolicy errorPolicy, - Object optimisticResult, - int pollInterval, - }) => - QueryOptions( - document: document ?? this.document, - operationName: operationName ?? this.operationName, - variables: variables ?? this.variables, - context: context ?? this.context, - fetchPolicy: fetchPolicy ?? this.fetchPolicy, - errorPolicy: errorPolicy ?? this.errorPolicy, - optimisticResult: optimisticResult ?? this.optimisticResult, - pollInterval: pollInterval ?? this.pollInterval, - ); -} - class SubscriptionOptions extends BaseOptions { SubscriptionOptions({ @required DocumentNode document, @@ -81,31 +54,8 @@ class SubscriptionOptions extends BaseOptions { context: context, optimisticResult: optimisticResult, ); - - SubscriptionOptions copyWith({ - DocumentNode document, - String operationName, - Map variables, - Context context, - FetchPolicy fetchPolicy, - ErrorPolicy errorPolicy, - Object optimisticResult, - }) => - SubscriptionOptions( - document: document ?? this.document, - operationName: operationName ?? this.operationName, - variables: variables ?? this.variables, - context: context ?? this.context, - fetchPolicy: fetchPolicy ?? this.fetchPolicy, - errorPolicy: errorPolicy ?? this.errorPolicy, - optimisticResult: optimisticResult ?? this.optimisticResult, - ); } -/// Mutation options - -// ObservableQuery options - class WatchQueryOptions extends QueryOptions { WatchQueryOptions({ @required DocumentNode document, @@ -131,40 +81,45 @@ class WatchQueryOptions extends QueryOptions { ); /// Whether or not to fetch results - final bool fetchResults; + bool fetchResults; - final bool eagerlyFetchResults; + bool eagerlyFetchResults; @override List get properties => [...super.properties, fetchResults, eagerlyFetchResults]; - WatchQueryOptions copyWith({ - DocumentNode document, - String operationName, - Map variables, - Context context, - FetchPolicy fetchPolicy, - ErrorPolicy errorPolicy, - Object optimisticResult, - int pollInterval, - bool fetchResults, - bool eagerlyFetchResults, - }) => - WatchQueryOptions( - document: document ?? this.document, - operationName: operationName ?? this.operationName, - variables: variables ?? this.variables, - context: context ?? this.context, - fetchPolicy: fetchPolicy ?? this.fetchPolicy, - errorPolicy: errorPolicy ?? this.errorPolicy, - optimisticResult: optimisticResult ?? this.optimisticResult, - pollInterval: pollInterval ?? this.pollInterval, - fetchResults: fetchResults ?? this.fetchResults, - eagerlyFetchResults: eagerlyFetchResults ?? this.eagerlyFetchResults, + WatchQueryOptions copy() => WatchQueryOptions( + document: document, + operationName: operationName, + variables: variables, + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, + optimisticResult: optimisticResult, + pollInterval: pollInterval, + fetchResults: fetchResults, + eagerlyFetchResults: eagerlyFetchResults, + context: context, ); } +/// options for fetchmore operations +class FetchMoreOptions { + FetchMoreOptions({ + @required this.document, + this.variables = const {}, + @required this.updateQuery, + }) : assert(updateQuery != null); + + DocumentNode document; + + final Map variables; + + /// Strategy for merging the fetchMore result data + /// with the result data already in the cache + UpdateQuery updateQuery; +} + /// merge fetchMore result data with earlier result data typedef dynamic UpdateQuery( dynamic previousResultData, diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index d7aced36f..58b0f3722 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -1,18 +1,11 @@ import 'package:meta/meta.dart'; import 'dart:async'; -import 'package:gql_exec/gql_exec.dart'; -import 'package:gql_link/gql_link.dart'; +import 'package:graphql/src/core/core.dart'; import 'package:graphql/src/cache/cache.dart'; -import 'package:graphql/src/core/observable_query.dart'; -import 'package:graphql/src/core/query_manager.dart'; -import 'package:graphql/src/core/query_options.dart'; -import 'package:graphql/src/core/query_result.dart'; import 'package:graphql/src/core/fetch_more.dart'; -import 'package:graphql/src/core/policies.dart' show DefaultPolicies; - /// The link is a [Link] over which GraphQL documents will be resolved into a [Response]. /// The cache is the initial [Cache] to use in the data store. class GraphQLClient { diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index 37263ed54..bbfaa1791 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -6,33 +6,6 @@ bool notNull(Object any) { return any != null; } -bool areDifferentVariables( - Map a, - Map b, -) { - if (a == null && b == null) { - return false; - } - - if (a == null || b == null) { - return true; - } - - if (a.length != b.length) { - return true; - } - - bool areDifferent = false; - - a.forEach((String key, dynamic value) { - if ((!b.containsKey(key)) || b[key] != value) { - areDifferent = true; - } - }); - - return areDifferent; -} - Map _recursivelyAddAll( Map target, Map source, diff --git a/packages/graphql/test/cache/cache_data.dart b/packages/graphql/test/cache/cache_data.dart index 993506b40..9f9f4e901 100644 --- a/packages/graphql/test/cache/cache_data.dart +++ b/packages/graphql/test/cache/cache_data.dart @@ -1,6 +1,5 @@ import 'package:gql_exec/gql_exec.dart'; import 'package:gql/language.dart'; -import 'package:graphql/internal.dart'; import 'package:meta/meta.dart'; const String rawOperationKey = 'rawOperationKey'; diff --git a/packages/graphql/test/cache/graphql_cache_test.dart b/packages/graphql/test/cache/graphql_cache_test.dart index d47786ce8..c8d9bf26b 100644 --- a/packages/graphql/test/cache/graphql_cache_test.dart +++ b/packages/graphql/test/cache/graphql_cache_test.dart @@ -12,7 +12,7 @@ void main() { cache.writeQuery(basicTest.request, basicTest.data); expect( cache.readQuery(basicTest.request), - equals(basicTest.data), + equal(basicTest.data), ); }); test('updating nested normalized fragment changes top level operation', () { @@ -26,7 +26,7 @@ void main() { ); expect( cache.readQuery(basicTest.request), - equals(updatedCBasicTestData), + equal(updatedCBasicTestData), ); }); @@ -37,7 +37,7 @@ void main() { ); expect( cache.readQuery(basicTest.request), - equals(updatedSubsetOperationData), + equal(updatedSubsetOperationData), ); }); }); @@ -48,7 +48,7 @@ void main() { cache.writeQuery(cyclicalTest.request, cyclicalTest.data); for (final normalized in cyclicalTest.normalizedEntities) { final dataId = "${normalized['__typename']}:${normalized['id']}"; - expect(cache.readNormalized(dataId), equals(normalized)); + expect(cache.readNormalized(dataId), equal(normalized)); } }); }); @@ -60,7 +60,7 @@ void main() { cache.writeQuery(cyclicalTest.request, cyclicalTest.data); for (final normalized in cyclicalTest.normalizedEntities) { final dataId = "${normalized['__typename']}:${normalized['id']}"; - expect(cache.readNormalized(dataId), equals(normalized)); + expect(cache.readNormalized(dataId), equal(normalized)); } }); }); @@ -82,7 +82,7 @@ void main() { ); expect( cache.readQuery(basicTest.request, optimistic: true), - equals(basicTest.data), + equal(basicTest.data), ); }, ); @@ -104,7 +104,7 @@ void main() { ); expect( cache.readQuery(basicTest.request), - equals(updatedCBasicTestData), + equal(updatedCBasicTestData), ); }, ); @@ -122,7 +122,7 @@ void main() { ); expect( cache.readQuery(basicTest.request, optimistic: true), - equals(updatedSubsetOperationData), + equal(updatedSubsetOperationData), ); }, ); @@ -134,7 +134,7 @@ void main() { cache.removeOptimisticPatch('3'); expect( cache.readQuery(basicTest.request, optimistic: true), - equals(basicTest.data), + equal(basicTest.data), ); }, ); From 6d0b04564148623ecfe75f376818250683522a4c Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 23 May 2020 18:49:51 -0500 Subject: [PATCH 031/118] feat(graphql): work on making subscriptions more of a first-class citizen --- .../graphql/lib/src/core/_data_class.dart | 2 +- packages/graphql/lib/src/core/policies.dart | 23 +++++++-- .../graphql/lib/src/core/query_manager.dart | 50 +++++++++++++++++-- .../graphql/lib/src/core/query_options.dart | 15 ++++++ packages/graphql/lib/src/graphql_client.dart | 7 ++- 5 files changed, 87 insertions(+), 10 deletions(-) diff --git a/packages/graphql/lib/src/core/_data_class.dart b/packages/graphql/lib/src/core/_data_class.dart index c7ea40009..37a51c1fd 100644 --- a/packages/graphql/lib/src/core/_data_class.dart +++ b/packages/graphql/lib/src/core/_data_class.dart @@ -15,7 +15,7 @@ abstract class MutableDataClass { (runtimeType == other.runtimeType && const ListEquality( DeepCollectionEquality(), - ).equal( + ).equals( other.properties, properties, )); diff --git a/packages/graphql/lib/src/core/policies.dart b/packages/graphql/lib/src/core/policies.dart index 53fe53608..03023da93 100644 --- a/packages/graphql/lib/src/core/policies.dart +++ b/packages/graphql/lib/src/core/policies.dart @@ -112,13 +112,25 @@ class DefaultPolicies { /// ``` final Policies mutate; + /// The default [Policies] for subscribe. + /// Defaults to + /// ``` + /// Policies( + /// FetchPolicy.cacheAndNetwork, + /// ErrorPolicy.none, + /// ) + /// ``` + final Policies subscribe; + DefaultPolicies({ Policies watchQuery, Policies query, Policies mutate, - }) : this.watchQuery = _watchQueryDefaults.withOverrides(watchQuery), - this.query = _queryDefaults.withOverrides(query), - this.mutate = _mutateDefaults.withOverrides(mutate); + Policies subscribe, + }) : watchQuery = _watchQueryDefaults.withOverrides(watchQuery), + query = _queryDefaults.withOverrides(query), + mutate = _mutateDefaults.withOverrides(mutate), + subscribe = _watchQueryDefaults.withOverrides(subscribe); static final _watchQueryDefaults = Policies.safe( FetchPolicy.cacheAndNetwork, @@ -138,17 +150,20 @@ class DefaultPolicies { Policies watchQuery, Policies query, Policies mutate, + Policies subscribe, }) => DefaultPolicies( watchQuery: watchQuery, query: query, mutate: mutate, + subscribe: subscribe, ); List _getChildren() => [ watchQuery, query, mutate, + subscribe, ]; @override @@ -157,7 +172,7 @@ class DefaultPolicies { (o is DefaultPolicies && const ListEquality( DeepCollectionEquality(), - ).equal( + ).equals( o._getChildren(), _getChildren(), )); diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 21d71f2a3..ed98f6bcf 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -45,6 +45,53 @@ class QueryManager { return observableQuery; } + Stream subscribe(SubscriptionOptions options) async* { + final request = options.asRequest; + + if (options.optimisticResult != null) { + // TODO optimisticResults for streams just skip the cache for now + yield QueryResult.optimistic(data: options.optimisticResult); + } else if (options.fetchPolicy != FetchPolicy.noCache) { + final cacheResult = cache.readQuery(request, optimistic: true); + if (cacheResult != null) { + yield QueryResult( + source: QueryResultSource.cache, + data: options.optimisticResult, + ); + } + } + + yield* link.request(request).map((response) { + QueryResult queryResult; + try { + if (response.data != null && + options.fetchPolicy != FetchPolicy.noCache) { + cache.writeQuery(request, data: response.data); + } + queryResult = mapFetchResultToQueryResult( + response, + options, + source: QueryResultSource.network, + ); + } catch (failure) { + // we set the source to indicate where the source of failure + queryResult ??= QueryResult(source: QueryResultSource.network); + + queryResult.exception = coalesceErrors( + exception: queryResult.exception, + linkException: translateFailure(failure), + ); + } + + if (options.fetchPolicy != FetchPolicy.noCache) { + // normalize results if previously written + queryResult.data = cache.readQuery(request); + } + + return queryResult; + }); + } + Future query(QueryOptions options) { return fetchQuery('0', options); } @@ -134,9 +181,6 @@ class QueryManager { source: QueryResultSource.network, ); } catch (failure) { - // TODO: handle Link exceptions - // TODO can we model this transformation as a link - // we set the source to indicate where the source of failure queryResult ??= QueryResult(source: QueryResultSource.network); diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index 1abccc5ab..dd1af6b79 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -34,6 +34,18 @@ class QueryOptions extends BaseOptions { @override List get properties => [...super.properties, pollInterval]; + + WatchQueryOptions asWatchQueryOptions({bool fetchResults = true}) => + WatchQueryOptions( + document: document, + variables: variables, + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, + pollInterval: pollInterval, + fetchResults: fetchResults ?? true, + context: context, + optimisticResult: optimisticResult, + ); } class SubscriptionOptions extends BaseOptions { @@ -54,6 +66,9 @@ class SubscriptionOptions extends BaseOptions { context: context, optimisticResult: optimisticResult, ); + + /// An optimistic first result to eagerly add to the subscription stream + Object optimisticResult; } class WatchQueryOptions extends QueryOptions { diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 58b0f3722..a74a377fb 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -57,8 +57,11 @@ class GraphQLClient { /// This subscribes to a GraphQL subscription according to the options specified and returns a /// [Stream] which either emits received data or an error. - Stream subscribe(Request request) { - return link.request(request); + Stream subscribe(SubscriptionOptions options) { + options.policies = defaultPolicies.subscribe.withOverrides( + options.policies, + ); + return queryManager.subscribe(options); } /// Fetch more results and then merge them with the given [previousResult] From a0e0d5c4f3439a98d9e249f8362d4115d2440efa Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 23 May 2020 18:50:09 -0500 Subject: [PATCH 032/118] feat(graphql_flutter): work on making subscriptions more of a first-class citizen --- .../lib/src/widgets/_subscription.dart | 108 --------------- .../lib/src/widgets/mutation.dart | 11 +- .../lib/src/widgets/query.dart | 20 +-- .../lib/src/widgets/subscription.dart | 126 ++++++------------ 4 files changed, 45 insertions(+), 220 deletions(-) delete mode 100644 packages/graphql_flutter/lib/src/widgets/_subscription.dart diff --git a/packages/graphql_flutter/lib/src/widgets/_subscription.dart b/packages/graphql_flutter/lib/src/widgets/_subscription.dart deleted file mode 100644 index c96baff21..000000000 --- a/packages/graphql_flutter/lib/src/widgets/_subscription.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import 'package:graphql/client.dart'; -import 'package:graphql/internal.dart'; - -import 'package:graphql_flutter/src/widgets/graphql_provider.dart'; - -// method to call from widget to fetchmore queries -typedef FetchMore = dynamic Function(FetchMoreOptions options); - -typedef Refetch = Future Function(); - -typedef QueryBuilder = Widget Function( - QueryResult result, { - Refetch refetch, - FetchMore fetchMore, -}); - -/// Builds a [Query] widget based on the a given set of [QueryOptions] -/// that streams [QueryResult]s into the [QueryBuilder]. -class Query extends StatefulWidget { - const Query({ - final Key key, - @required this.options, - @required this.builder, - }) : super(key: key); - - final QueryOptions options; - final QueryBuilder builder; - - @override - QueryState createState() => QueryState(); -} - -class QueryState extends State { - ObservableQuery observableQuery; - GraphQLClient _client; - WatchQueryOptions get _options { - final QueryOptions options = widget.options; - - return WatchQueryOptions( - document: options.document, - variables: options.variables, - fetchPolicy: options.fetchPolicy, - errorPolicy: options.errorPolicy, - pollInterval: options.pollInterval, - fetchResults: true, - context: options.context, - optimisticResult: options.optimisticResult, - ); - } - - void _initQuery() { - observableQuery?.close(); - observableQuery = _client.watchQuery(_options); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final GraphQLClient client = GraphQLProvider.of(context).value; - assert(client != null); - if (client != _client) { - _client = client; - _initQuery(); - } - } - - @override - void didUpdateWidget(Query oldWidget) { - super.didUpdateWidget(oldWidget); - - final GraphQLClient client = GraphQLProvider.of(context).value; - - final optionsWithOverrides = _options; - optionsWithOverrides.policies = client.defaultPolicies.watchQuery - .withOverrides(optionsWithOverrides.policies); - - if (!observableQuery.options.areEqualTo(optionsWithOverrides)) { - _initQuery(); - } - } - - @override - void dispose() { - observableQuery?.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return StreamBuilder( - key: Key(observableQuery?.options?.toKey()), - initialData: observableQuery?.latestResult ?? QueryResult.loading(), - stream: observableQuery.stream, - builder: ( - BuildContext buildContext, - AsyncSnapshot snapshot, - ) { - return widget?.builder( - snapshot.data, - refetch: observableQuery.refetch, - fetchMore: observableQuery.fetchMore, - ); - }, - ); - } -} diff --git a/packages/graphql_flutter/lib/src/widgets/mutation.dart b/packages/graphql_flutter/lib/src/widgets/mutation.dart index d6dcba9f4..cf9572f70 100644 --- a/packages/graphql_flutter/lib/src/widgets/mutation.dart +++ b/packages/graphql_flutter/lib/src/widgets/mutation.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:graphql/client.dart'; -import 'package:graphql/internal.dart'; import 'package:graphql_flutter/src/widgets/graphql_provider.dart'; @@ -54,7 +53,7 @@ class MutationState extends State { bool _setNewOptions() { final _cached = __cachedOptions; final _new = _providedOptions; - if (_cached == null || !_new.areEqualTo(_cached)) { + if (_cached == null || !_new.equal(_cached)) { __cachedOptions = _new; return true; } @@ -94,7 +93,7 @@ class MutationState extends State { Map variables, { Object optimisticResult, }) { - final mutationCallbacks = MutationCallbacks( + final mutationCallbacks = MutationCallbackHandler( cache: client.cache, queryId: observableQuery.queryId, options: widget.options, @@ -118,11 +117,7 @@ class MutationState extends State { @override Widget build(BuildContext context) { return StreamBuilder( - // we give the stream builder a key so that - // toggling mutations at the same place in the tree, - // such as is done in the example, won't result in bugs - key: Key(observableQuery?.options?.toKey()), - initialData: observableQuery?.latestResult ?? QueryResult(), + initialData: observableQuery?.latestResult ?? QueryResult(source: null), stream: observableQuery?.stream, builder: ( BuildContext buildContext, diff --git a/packages/graphql_flutter/lib/src/widgets/query.dart b/packages/graphql_flutter/lib/src/widgets/query.dart index c96baff21..757d65640 100644 --- a/packages/graphql_flutter/lib/src/widgets/query.dart +++ b/packages/graphql_flutter/lib/src/widgets/query.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:graphql/client.dart'; -import 'package:graphql/internal.dart'; import 'package:graphql_flutter/src/widgets/graphql_provider.dart'; @@ -35,20 +34,8 @@ class Query extends StatefulWidget { class QueryState extends State { ObservableQuery observableQuery; GraphQLClient _client; - WatchQueryOptions get _options { - final QueryOptions options = widget.options; - - return WatchQueryOptions( - document: options.document, - variables: options.variables, - fetchPolicy: options.fetchPolicy, - errorPolicy: options.errorPolicy, - pollInterval: options.pollInterval, - fetchResults: true, - context: options.context, - optimisticResult: options.optimisticResult, - ); - } + + WatchQueryOptions get _options => widget.options.asWatchQueryOptions(); void _initQuery() { observableQuery?.close(); @@ -76,7 +63,7 @@ class QueryState extends State { optionsWithOverrides.policies = client.defaultPolicies.watchQuery .withOverrides(optionsWithOverrides.policies); - if (!observableQuery.options.areEqualTo(optionsWithOverrides)) { + if (!observableQuery.options.equal(optionsWithOverrides)) { _initQuery(); } } @@ -90,7 +77,6 @@ class QueryState extends State { @override Widget build(BuildContext context) { return StreamBuilder( - key: Key(observableQuery?.options?.toKey()), initialData: observableQuery?.latestResult ?? QueryResult.loading(), stream: observableQuery.stream, builder: ( diff --git a/packages/graphql_flutter/lib/src/widgets/subscription.dart b/packages/graphql_flutter/lib/src/widgets/subscription.dart index 00efdf854..974de04a4 100644 --- a/packages/graphql_flutter/lib/src/widgets/subscription.dart +++ b/packages/graphql_flutter/lib/src/widgets/subscription.dart @@ -3,48 +3,36 @@ import 'dart:io'; import 'package:connectivity/connectivity.dart'; import 'package:flutter/widgets.dart'; -import 'package:gql/language.dart'; -import 'package:gql_exec/gql_exec.dart'; import 'package:graphql/client.dart'; -import 'package:graphql/internal.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:graphql_flutter/src/widgets/graphql_provider.dart'; -typedef OnSubscriptionCompleted = void Function(); +typedef OnSubscriptionResult = void Function( + QueryResult subscriptionResult, + GraphQLClient client, +); -typedef SubscriptionBuilder = Widget Function({ - bool loading, - T payload, - dynamic error, -}); +typedef SubscriptionBuilder = Widget Function(QueryResult result); class Subscription extends StatefulWidget { - const Subscription( - this.operationName, - this.query, { - this.variables = const {}, - final Key key, + const Subscription({ + @required this.options, @required this.builder, - this.initial, - this.onCompleted, + this.onSubscriptionResult, + Key key, }) : super(key: key); - final String operationName; - final String query; - final Map variables; - final SubscriptionBuilder builder; - final OnSubscriptionCompleted onCompleted; - final T initial; + final SubscriptionOptions options; + final SubscriptionBuilder builder; + final OnSubscriptionResult onSubscriptionResult; @override _SubscriptionState createState() => _SubscriptionState(); } class _SubscriptionState extends State> { - bool _loading = true; - T _data; - dynamic _error; - StreamSubscription _subscription; - GraphQLClient _client; + Stream stream; + GraphQLClient client; ConnectivityResult _currentConnectivityResult; StreamSubscription _networkSubscription; @@ -52,39 +40,21 @@ class _SubscriptionState extends State> { void _initSubscription() { final GraphQLClient client = GraphQLProvider.of(context).value; assert(client != null); - final Request request = Request( - operation: Operation( - document: parseString(widget.query), - operationName: widget.operationName, - ), - variables: widget.variables, - ); - final Stream stream = _client.subscribe(request); + stream = client.subscribe(widget.options); - if (_subscription == null) { - // Set the initial value for the first time. - if (widget.initial != null) { - setState(() { - _loading = true; - _data = widget.initial; - _error = null; - }); - } + if (widget.onSubscriptionResult != null) { + stream = stream.map((result) { + widget.onSubscriptionResult(result, client); + return result; + }); } - - _subscription?.cancel(); - _subscription = stream.listen( - _onData, - onError: _onError, - onDone: _onDone, - ); } @override void initState() { - _networkSubscription = Connectivity().onConnectivityChanged.listen( - (ConnectivityResult result) async => await _onNetworkChange(result)); + _networkSubscription = + Connectivity().onConnectivityChanged.listen(_onNetworkChange); super.initState(); } @@ -92,10 +62,10 @@ class _SubscriptionState extends State> { @override void didChangeDependencies() { super.didChangeDependencies(); - final GraphQLClient client = GraphQLProvider.of(context).value; - assert(client != null); - if (client != _client) { - _client = client; + final GraphQLClient newClient = GraphQLProvider.of(context).value; + assert(newClient != null); + if (client != newClient) { + client = newClient; _initSubscription(); } } @@ -104,42 +74,17 @@ class _SubscriptionState extends State> { void didUpdateWidget(Subscription oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.query != oldWidget.query || - widget.operationName != oldWidget.operationName || - areDifferentVariables(widget.variables, oldWidget.variables)) { + if (!widget.options.equal(oldWidget.options)) { _initSubscription(); } } @override void dispose() { - _subscription?.cancel(); _networkSubscription?.cancel(); super.dispose(); } - void _onData(final Response message) { - setState(() { - _loading = false; - _data = message.data as T; - _error = message.errors; - }); - } - - void _onError(final Object error) { - setState(() { - _loading = false; - _data = null; - _error = error; - }); - } - - void _onDone() { - if (widget.onCompleted != null) { - widget.onCompleted(); - } - } - Future _onNetworkChange(ConnectivityResult result) async { //if from offline to online if (_currentConnectivityResult == ConnectivityResult.none && @@ -170,10 +115,17 @@ class _SubscriptionState extends State> { @override Widget build(final BuildContext context) { - return widget.builder( - loading: _loading, - error: _error, - payload: _data, + return StreamBuilder( + initialData: widget.options?.optimisticResult != null + ? QueryResult.optimistic(data: widget.options?.optimisticResult) + : QueryResult.loading(), + stream: stream, + builder: ( + BuildContext buildContext, + AsyncSnapshot snapshot, + ) { + return widget?.builder(snapshot.data); + }, ); } } From 82724f08b58db163864426e22b64ae159e6ef1e3 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 23 May 2020 18:50:55 -0500 Subject: [PATCH 033/118] fix(examples): cleanup bloc example --- examples/flutter_bloc/lib/extended_bloc/graphql/bloc.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/flutter_bloc/lib/extended_bloc/graphql/bloc.dart b/examples/flutter_bloc/lib/extended_bloc/graphql/bloc.dart index f5da809c5..2bbaf1ed7 100644 --- a/examples/flutter_bloc/lib/extended_bloc/graphql/bloc.dart +++ b/examples/flutter_bloc/lib/extended_bloc/graphql/bloc.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:graphql/client.dart'; import 'package:meta/meta.dart'; -import 'package:graphql/internal.dart'; import 'package:graphql_flutter_bloc_example/extended_bloc/graphql/event.dart'; import 'package:graphql_flutter_bloc_example/extended_bloc/graphql/state.dart'; From bba4a7aba99bd3e4c5c49442f2e1e6e3cc71cb67 Mon Sep 17 00:00:00 2001 From: micimize Date: Sun, 24 May 2020 10:08:19 -0500 Subject: [PATCH 034/118] fix(tests): update tests --- packages/graphql/test/cache/cache_data.dart | 1 + .../test/cache/graphql_cache_test.dart | 30 ++--- .../test/core/raw_operation_data_test.dart | 127 ------------------ .../graphql/test/graphql_client_test.dart | 4 +- 4 files changed, 18 insertions(+), 144 deletions(-) delete mode 100644 packages/graphql/test/core/raw_operation_data_test.dart diff --git a/packages/graphql/test/cache/cache_data.dart b/packages/graphql/test/cache/cache_data.dart index 9f9f4e901..029704e96 100644 --- a/packages/graphql/test/cache/cache_data.dart +++ b/packages/graphql/test/cache/cache_data.dart @@ -1,5 +1,6 @@ import 'package:gql_exec/gql_exec.dart'; import 'package:gql/language.dart'; +import 'package:graphql/src/utilities/helpers.dart'; import 'package:meta/meta.dart'; const String rawOperationKey = 'rawOperationKey'; diff --git a/packages/graphql/test/cache/graphql_cache_test.dart b/packages/graphql/test/cache/graphql_cache_test.dart index c8d9bf26b..af9d44842 100644 --- a/packages/graphql/test/cache/graphql_cache_test.dart +++ b/packages/graphql/test/cache/graphql_cache_test.dart @@ -9,10 +9,10 @@ void main() { group('Normalizes writes', () { final GraphQLCache cache = getTestCache(); test('.writeQuery .readQuery round trip', () { - cache.writeQuery(basicTest.request, basicTest.data); + cache.writeQuery(basicTest.request, data: basicTest.data); expect( cache.readQuery(basicTest.request), - equal(basicTest.data), + equals(basicTest.data), ); }); test('updating nested normalized fragment changes top level operation', () { @@ -26,18 +26,18 @@ void main() { ); expect( cache.readQuery(basicTest.request), - equal(updatedCBasicTestData), + equals(updatedCBasicTestData), ); }); test('updating subset query only partially overrides superset query', () { cache.writeQuery( basicTestSubsetAValue.request, - basicTestSubsetAValue.data, + data: basicTestSubsetAValue.data, ); expect( cache.readQuery(basicTest.request), - equal(updatedSubsetOperationData), + equals(updatedSubsetOperationData), ); }); }); @@ -45,10 +45,10 @@ void main() { group('Handles cyclical references', () { final GraphQLCache cache = getTestCache(); test('lazily reads cyclical references', () { - cache.writeQuery(cyclicalTest.request, cyclicalTest.data); + cache.writeQuery(cyclicalTest.request, data: cyclicalTest.data); for (final normalized in cyclicalTest.normalizedEntities) { final dataId = "${normalized['__typename']}:${normalized['id']}"; - expect(cache.readNormalized(dataId), equal(normalized)); + expect(cache.readNormalized(dataId), equals(normalized)); } }); }); @@ -57,10 +57,10 @@ void main() { final GraphQLCache cache = getTestCache(); test('correctly reads cyclical references', () { cyclicalTest.data = cyclicalObjOperationData; - cache.writeQuery(cyclicalTest.request, cyclicalTest.data); + cache.writeQuery(cyclicalTest.request, data: cyclicalTest.data); for (final normalized in cyclicalTest.normalizedEntities) { final dataId = "${normalized['__typename']}:${normalized['id']}"; - expect(cache.readNormalized(dataId), equal(normalized)); + expect(cache.readNormalized(dataId), equals(normalized)); } }); }); @@ -76,13 +76,13 @@ void main() { (proxy) => proxy ..writeQuery( basicTest.request, - basicTest.data, + data: basicTest.data, ), '1', ); expect( cache.readQuery(basicTest.request, optimistic: true), - equal(basicTest.data), + equals(basicTest.data), ); }, ); @@ -104,7 +104,7 @@ void main() { ); expect( cache.readQuery(basicTest.request), - equal(updatedCBasicTestData), + equals(updatedCBasicTestData), ); }, ); @@ -116,13 +116,13 @@ void main() { (proxy) => proxy ..writeQuery( basicTestSubsetAValue.request, - basicTestSubsetAValue.data, + data: basicTestSubsetAValue.data, ), '3', ); expect( cache.readQuery(basicTest.request, optimistic: true), - equal(updatedSubsetOperationData), + equals(updatedSubsetOperationData), ); }, ); @@ -134,7 +134,7 @@ void main() { cache.removeOptimisticPatch('3'); expect( cache.readQuery(basicTest.request, optimistic: true), - equal(basicTest.data), + equals(basicTest.data), ); }, ); diff --git a/packages/graphql/test/core/raw_operation_data_test.dart b/packages/graphql/test/core/raw_operation_data_test.dart deleted file mode 100644 index 60fe97622..000000000 --- a/packages/graphql/test/core/raw_operation_data_test.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:gql/language.dart'; -import 'package:graphql/src/core/raw_operation_data.dart'; -import 'package:test/test.dart'; - -void main() { - group('operation name', () { - group('single operation', () { - test('query without name', () { - final opData = RawOperationData( - document: parseString('query {}'), - ); - - expect(opData.operationName, null); - }); - - test('query with explicit name', () { - final opData = RawOperationData( - document: parseString('query Operation {}'), - operationName: 'Operation', - ); - - expect(opData.operationName, 'Operation'); - }); - - test('mutation with explicit name', () { - final opData = RawOperationData( - document: parseString('mutation Operation {}'), - operationName: 'Operation', - ); - - expect(opData.operationName, 'Operation'); - }); - - test('subscription with explicit name', () { - final opData = RawOperationData( - document: parseString('subscription Operation {}'), - operationName: 'Operation', - ); - - expect(opData.operationName, 'Operation'); - }); - - test('query with implicit name', () { - final opData = RawOperationData( - document: parseString('query Operation {}'), - ); - - expect(opData.operationName, 'Operation'); - }); - - test('mutation with implicit name', () { - final opData = RawOperationData( - document: parseString('mutation Operation {}'), - ); - - expect(opData.operationName, 'Operation'); - }); - - test('subscription with implicit name', () { - final opData = RawOperationData( - document: parseString('subscription Operation {}'), - ); - - expect(opData.operationName, 'Operation'); - }); - }); - - group('multiple operations', () { - const document = r''' - query OperationQ {} - mutation OperationM {} - subscription OperationS {} - '''; - - test('query with explicit name', () { - final opData = RawOperationData( - document: parseString(document), - operationName: 'OperationQ', - ); - - expect(opData.operationName, 'OperationQ'); - }); - - test('mutation with explicit name', () { - final opData = RawOperationData( - document: parseString(document), - operationName: 'OperationM', - ); - - expect(opData.operationName, 'OperationM'); - }); - - test('subscription with explicit name', () { - final opData = RawOperationData( - document: parseString(document), - operationName: 'OperationS', - ); - - expect(opData.operationName, 'OperationS'); - }); - - test('query with implicit name', () { - final opData = RawOperationData( - document: parseString(document), - ); - - expect(opData.operationName, 'OperationS'); - }); - - test('mutation with implicit name', () { - final opData = RawOperationData( - document: parseString(document), - ); - - expect(opData.operationName, 'OperationS'); - }); - - test('subscription with implicit name', () { - final opData = RawOperationData( - document: parseString(document), - ); - - expect(opData.operationName, 'OperationS'); - }); - }); - }); -} diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index 0d983c75a..b8578b69c 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -140,7 +140,7 @@ void main() { ); expect( - (r.exception.clientException as UnhandledFailureWrapper).failure, + r.exception.linkException.originalException, e, ); @@ -163,7 +163,7 @@ void main() { ); expect( - (r.exception.clientException as UnhandledFailureWrapper).failure, + r.exception.linkException.originalException, e, ); From 13df194b4b060fed9020b7392998bbee7e2368d0 Mon Sep 17 00:00:00 2001 From: micimize Date: Sun, 31 May 2020 12:10:39 -0500 Subject: [PATCH 035/118] refactor(graphql): deprecating SCREAMING_CASE enums --- packages/graphql/lib/client.dart | 4 +- .../src/cache/_normalizing_data_proxy.dart | 2 +- .../lib/src/core/observable_query.dart | 194 +++++++++++++----- .../graphql/lib/src/core/query_result.dart | 28 ++- packages/graphql/lib/src/graphql_client.dart | 44 ++++ 5 files changed, 205 insertions(+), 67 deletions(-) diff --git a/packages/graphql/lib/client.dart b/packages/graphql/lib/client.dart index 6d9d31fda..1428bd2fb 100644 --- a/packages/graphql/lib/client.dart +++ b/packages/graphql/lib/client.dart @@ -1,9 +1,7 @@ library graphql; export 'package:graphql/src/cache/cache.dart'; -export 'package:graphql/src/core/query_manager.dart'; -export 'package:graphql/src/core/query_options.dart'; -export 'package:graphql/src/core/fetch_more.dart' show FetchMoreOptions; +export 'package:graphql/src/core/core.dart'; export 'package:graphql/src/core/query_result.dart'; export 'package:graphql/src/core/policies.dart'; export 'package:graphql/src/exceptions.dart'; diff --git a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart index a8a93f880..e41dc3521 100644 --- a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart @@ -32,7 +32,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { /// Flag used to request a (re)broadcast from the [QueryManager] @protected - bool broadcastRequested; + bool broadcastRequested = false; /// Optional `dataIdFromObject` function to pass through to [normalize] DataIdResolver dataIdFromObject; diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 02235aa57..29584c82d 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -10,19 +10,91 @@ import 'package:graphql/src/scheduler/scheduler.dart'; typedef OnData = void Function(QueryResult result); +/// lifecycle states for [ObservableQuery.lifecycle] enum QueryLifecycle { - UNEXECUTED, - PENDING, - POLLING, - POLLING_STOPPED, - SIDE_EFFECTS_PENDING, - SIDE_EFFECTS_BLOCKING, - - COMPLETED, - CLOSED + /// No results have been requested or fetched + unexecuted, + + /// Results are being fetched, and will be side-effect free + pending, + + /// Polling for results periodically + polling, + + /// [Observab] + pollingStopped, + + /// Results are being fetched, and will trigger + /// the callbacks registered with [ObservableQuery.onData] + sideEffectsPending, + + /// Pending side effects are preventing [ObservableQuery.close], + /// and the [ObservableQuery] will be discarded after fetch completes + /// and side effects are resolved. + sideEffectsBlocking, + + /// The operation was executed and is not [polling] + completed, + + /// [ObservableQuery.close] was called and all activity + /// from this [ObservableQuery] has ceased. + closed +} + +extension DeprecatedQueryLifecycle on QueryLifecycle { + /// No data has been specified from any source + @Deprecated( + 'Use `QueryLifecycle.unexecuted` instead. Will be removed in 5.0.0') + static const UNEXECUTED = QueryLifecycle.unexecuted; + + @Deprecated('Use `QueryLifecycle.pending` instead. Will be removed in 5.0.0') + static QueryLifecycle get PENDING => QueryLifecycle.pending; + + @Deprecated('Use `QueryLifecycle.polling` instead. Will be removed in 5.0.0') + static QueryLifecycle get POLLING => QueryLifecycle.polling; + + @Deprecated( + 'Use `QueryLifecycle.pollingStopped` instead. Will be removed in 5.0.0') + static QueryLifecycle get POLLING_STOPPED => QueryLifecycle.pollingStopped; + + @Deprecated( + 'Use `QueryLifecycle.sideEffectsPending` instead. Will be removed in 5.0.0') + static QueryLifecycle get SIDE_EFFECTS_PENDING => + QueryLifecycle.sideEffectsPending; + + @Deprecated( + 'Use `QueryLifecycle.sideEffectsBlocking` instead. Will be removed in 5.0.0') + static const SIDE_EFFECTS_BLOCKING = QueryLifecycle.sideEffectsBlocking; + + @Deprecated( + 'Use `QueryLifecycle.completed` instead. Will be removed in 5.0.0') + static QueryLifecycle get COMPLETED => QueryLifecycle.completed; + + @Deprecated( + 'Use `QueryLifecycle.completed` instead. Will be removed in 5.0.0') + static QueryLifecycle get CLOSED => QueryLifecycle.closed; } -/// An Observable/Stream-based API returned from `watchQuery` for use in reactive programming +/// An Observable/Stream-based API for both queries and mutations. +/// Returned from [GraphQLClient.watchQuery] for use in reactive programming, +/// for instance in `graphql_flutter` widgets. +/// +/// [ObservableQuery]'s core api/usage is to [fetchResults], then listen to the [stream]. +/// [fetchResults] will be called on instantiation if [options.eagerlyFetchResults] is set, +/// which in turn defaults to [options.fetchResults]. +/// +/// Beyond that, [ObservableQuery] is a bit of a kitchen sink: +/// * There are [refetch] and [fetchMore] methods for fetching more results +/// * [onData] +/// +/// +/// It has +/// +/// Results can be [refetch]ed, +/// +/// It is currently used +/// * [lifecycle] for tracking polling, side effect, an inflight execution state +/// * [latestResult] – the most recent result from this operation /// /// Modelled closely after [Apollo's ObservableQuery][apollo_oq] /// @@ -56,26 +128,26 @@ class ObservableQuery { /// The most recently seen result from this operation's stream QueryResult latestResult; - QueryLifecycle lifecycle = QueryLifecycle.UNEXECUTED; + QueryLifecycle lifecycle = QueryLifecycle.unexecuted; WatchQueryOptions options; StreamController controller; Stream get stream => controller.stream; - bool get isCurrentlyPolling => lifecycle == QueryLifecycle.POLLING; + bool get isCurrentlyPolling => lifecycle == QueryLifecycle.polling; bool get _isRefetchSafe { switch (lifecycle) { - case QueryLifecycle.COMPLETED: - case QueryLifecycle.POLLING: - case QueryLifecycle.POLLING_STOPPED: + case QueryLifecycle.completed: + case QueryLifecycle.polling: + case QueryLifecycle.pollingStopped: return true; - case QueryLifecycle.PENDING: - case QueryLifecycle.CLOSED: - case QueryLifecycle.UNEXECUTED: - case QueryLifecycle.SIDE_EFFECTS_PENDING: + case QueryLifecycle.pending: + case QueryLifecycle.closed: + case QueryLifecycle.unexecuted: + case QueryLifecycle.sideEffectsPending: case QueryLifecycle.SIDE_EFFECTS_BLOCKING: return false; } @@ -92,16 +164,16 @@ class ObservableQuery { bool get isRebroadcastSafe { switch (lifecycle) { - case QueryLifecycle.PENDING: - case QueryLifecycle.COMPLETED: - case QueryLifecycle.POLLING: - case QueryLifecycle.POLLING_STOPPED: + case QueryLifecycle.pending: + case QueryLifecycle.completed: + case QueryLifecycle.polling: + case QueryLifecycle.pollingStopped: return true; - case QueryLifecycle.UNEXECUTED: // this might be ok - case QueryLifecycle.CLOSED: - case QueryLifecycle.SIDE_EFFECTS_PENDING: - case QueryLifecycle.SIDE_EFFECTS_BLOCKING: + case QueryLifecycle.unexecuted: // this might be ok + case QueryLifecycle.closed: + case QueryLifecycle.sideEffectsPending: + case QueryLifecycle.sideEffectsBlocking: return false; } return false; @@ -132,8 +204,8 @@ class ObservableQuery { // if onData callbacks have been registered, // they are waited on by default lifecycle = _onDataSubscriptions.isNotEmpty - ? QueryLifecycle.SIDE_EFFECTS_PENDING - : QueryLifecycle.PENDING; + ? QueryLifecycle.sideEffectsPending + : QueryLifecycle.pending; if (options.pollInterval != null && options.pollInterval > 0) { startPolling(options.pollInterval); @@ -144,7 +216,9 @@ class ObservableQuery { /// fetch more results and then merge them with the [latestResult] /// according to [FetchMoreOptions.updateQuery]. - /// The results will then be added to to stream for the widget to re-build + /// + /// The results will then be added to to stream for listeners to react to, + /// such as for triggering `grahphql_flutter` widget rebuilds Future fetchMore(FetchMoreOptions fetchMoreOptions) async { assert(fetchMoreOptions.updateQuery != null); @@ -173,8 +247,8 @@ class ObservableQuery { result.source ??= latestResult.source; } - if (lifecycle == QueryLifecycle.PENDING && !result.isOptimistic) { - lifecycle = QueryLifecycle.COMPLETED; + if (lifecycle == QueryLifecycle.pending && !result.isOptimistic) { + lifecycle = QueryLifecycle.completed; } latestResult = result; @@ -185,31 +259,37 @@ class ObservableQuery { } // most mutation behavior happens here - /// call any registered callbacks, then rebroadcast queries - /// incase the underlying data has changed + /// Register [callbacks] to trigger when [stream] has new results + /// where [QueryResult.isNotLoading] + /// + /// Will deregister [callbacks] after calling them on the first + /// result that [QueryResult.isConcrete], + /// handling the resolution of [lifecycle] from + /// [QueryLifecycle.sideEffectsBlocking] to [QueryLifecycle.completed] + /// as appropriate void onData(Iterable callbacks) { callbacks ??= const []; StreamSubscription subscription; - subscription = stream.listen((QueryResult result) async { - if (!result.isLoading) { + subscription = stream.where((result) => result.isNotLoading).listen( + (QueryResult result) async { for (final callback in callbacks) { await callback(result); } - if (!result.isOptimistic) { + if (result.isConcrete) { await subscription.cancel(); _onDataSubscriptions.remove(subscription); if (_onDataSubscriptions.isEmpty) { - if (lifecycle == QueryLifecycle.SIDE_EFFECTS_BLOCKING) { - lifecycle = QueryLifecycle.COMPLETED; + if (lifecycle == QueryLifecycle.sideEffectsBlocking) { + lifecycle = QueryLifecycle.completed; close(); } } } - } - }); + }, + ); _onDataSubscriptions.add(subscription); } @@ -227,7 +307,7 @@ class ObservableQuery { } options.pollInterval = pollInterval; - lifecycle = QueryLifecycle.POLLING; + lifecycle = QueryLifecycle.polling; scheduler.startPollingQuery(options, queryId); } @@ -235,28 +315,34 @@ class ObservableQuery { if (isCurrentlyPolling) { scheduler.stopPollingQuery(queryId); options.pollInterval = null; - lifecycle = QueryLifecycle.POLLING_STOPPED; + lifecycle = QueryLifecycle.pollingStopped; } } - set variables(Map variables) { - options.variables = variables; - } + set variables(Map variables) => + options.variables = variables; + + /// [onData] callbacks have het to be run + /// + /// inlcudes `lifecycle == QueryLifecycle.sideEffectsBlocking` + bool get sideEffectsArePending => + (lifecycle == QueryLifecycle.sideEffectsPending || + lifecycle == QueryLifecycle.sideEffectsBlocking); /// Closes the query or mutation, or else queues it for closing. /// - /// To preserve Mutation side effects, `close` checks the `lifecycle`, - /// queuing the stream for closing if `lifecycle == QueryLifecycle.SIDE_EFFECTS_PENDING`. + /// To preserve Mutation side effects, [close] checks the [lifecycle], + /// queuing the stream for closing if [sideEffectsArePending]. /// You can override this check with `force: true`. /// - /// Returns a `FutureOr` of the resultant lifecycle - /// (`QueryLifecycle.SIDE_EFFECTS_BLOCKING | QueryLifecycle.CLOSED`) + /// Returns a [FutureOr] of the resultant lifecycle, either + /// [QueryLifecycle.sideEffectsBlocking] or [QueryLifecycle.closed] FutureOr close({ bool force = false, bool fromManager = false, }) async { - if (lifecycle == QueryLifecycle.SIDE_EFFECTS_PENDING && !force) { - lifecycle = QueryLifecycle.SIDE_EFFECTS_BLOCKING; + if (lifecycle == QueryLifecycle.sideEffectsPending && !force) { + lifecycle = QueryLifecycle.sideEffectsBlocking; // stop closing because we're waiting on something return lifecycle; } @@ -274,7 +360,7 @@ class ObservableQuery { await controller.close(); - lifecycle = QueryLifecycle.CLOSED; - return QueryLifecycle.CLOSED; + lifecycle = QueryLifecycle.closed; + return QueryLifecycle.closed; } } diff --git a/packages/graphql/lib/src/core/query_result.dart b/packages/graphql/lib/src/core/query_result.dart index 8d4bea75f..71b9cc8ae 100644 --- a/packages/graphql/lib/src/core/query_result.dart +++ b/packages/graphql/lib/src/core/query_result.dart @@ -28,29 +28,30 @@ enum QueryResultSource { network, } -extension on QueryResultSource { +extension Getters on QueryResultSource { /// Whether this result source is considered "eager" (is [cache] or [optimisticResult]) bool get isEager => _eagerSources.contains(this); /// No data has been specified from any source @Deprecated( 'Use `QueryResultSource.loading` instead. Will be removed in 5.0.0') - QueryResultSource get Loading => QueryResultSource.loading; + static QueryResultSource get Loading => QueryResultSource.loading; /// A result has been eagerly resolved from the cache @Deprecated('Use `QueryResultSource.cache` instead. Will be removed in 5.0.0') - QueryResultSource get Cache => QueryResultSource.cache; + static QueryResultSource get Cache => QueryResultSource.cache; /// An optimistic result has been specified. /// May include eager results from the cache @Deprecated( 'Use `QueryResultSource.optimisticResult` instead. Will be removed in 5.0.0') - QueryResultSource get OptimisticResult => QueryResultSource.optimisticResult; + static QueryResultSource get OptimisticResult => + QueryResultSource.optimisticResult; /// The query has been resolved on the network @Deprecated( 'Use `QueryResultSource.network` instead. Will be removed in 5.0.0') - QueryResultSource get Network => QueryResultSource.network; + static QueryResultSource get Network => QueryResultSource.network; } final _eagerSources = { @@ -94,19 +95,28 @@ class QueryResult { OperationException exception; - /// Whether [data] has yet to be specified from either the cache or network + /// [data] has yet to be specified from any source + /// (including [QueryResultSource.optimisticResult]) bool get isLoading => source == QueryResultSource.loading; - /// Whether [data] has yet to be specified from either the cache or network + /// [data] been specified (including [QueryResultSource.optimisticResult]) + bool get isNotLoading => !isLoading; + + /// [data] been specified (including [QueryResultSource.optimisticResult]) @Deprecated('Use `isLoading` instead. Will be removed in 5.0.0') bool get loading => isLoading; - /// Whether an optimistic result has been specified. + /// [data] has been specified as an [QueryResultSource.optimisticResult] /// /// May include eager results from the cache. bool get isOptimistic => source == QueryResultSource.optimisticResult; - /// Whether an optimistic result has been specified. + /// [data] has been specified and is **not** an [QueryResultSource.optimisticResult] + /// + /// shorthand for `!isLoading && !isOptimistic` + bool get isConcrete => !isLoading && !isOptimistic; + + /// [data] has been specified as an [QueryResultSource.optimisticResult] /// /// May include eager results from the cache. @Deprecated('Use `isOptimistic` instead. Will be removed in 5.0.0') diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index a74a377fb..3054678c5 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -35,6 +35,50 @@ class GraphQLClient { /// This registers a query in the [QueryManager] and returns an [ObservableQuery] /// based on the provided [WatchQueryOptions]. + /// + /// {@tool snippet} + /// + /// Basic usage + /// ```dart + /// + /// result = client.watchQuery(WatchQueryOptions( + /// options: QueryOptions( + /// document: gql(r''' + /// query HeroForEpisode($ep: Episode!) { + /// hero(episode: $ep) { + /// __typename + /// name + /// ... on Droid { + /// primaryFunction + /// } + /// ... on Human { + /// height + /// homePlanet + /// } + /// } + /// } + /// '''), + /// variables: { + /// 'ep': episodeToJson(episode), + /// }, + /// ), + /// )); + /// + /// result.stream.listen((QueryResult result) { + /// if (!result.loading && result.data != null) { + /// add( + /// GraphqlLoadedEvent( + /// data: parseData(result.data as Map), + /// result: result, + /// ), + /// ); + /// } + /// if (result.hasException) { + /// add(GraphqlErrorEvent(error: result.exception, result: result)); + /// } + /// }); + /// ``` + /// {@end-tool} ObservableQuery watchQuery(WatchQueryOptions options) { options.policies = defaultPolicies.watchQuery.withOverrides(options.policies); From b9d214c9045971be4e379210afbad856555a598f Mon Sep 17 00:00:00 2001 From: micimize Date: Sun, 31 May 2020 12:13:13 -0500 Subject: [PATCH 036/118] refactor(graphql): decided to make lifecycle refactor breaking because the api was not well documented anyhow --- .../lib/src/core/observable_query.dart | 38 +------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 29584c82d..8fc0a7e58 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -10,7 +10,7 @@ import 'package:graphql/src/scheduler/scheduler.dart'; typedef OnData = void Function(QueryResult result); -/// lifecycle states for [ObservableQuery.lifecycle] +/// Lifecycle states for [ObservableQuery.lifecycle] enum QueryLifecycle { /// No results have been requested or fetched unexecuted, @@ -41,40 +41,6 @@ enum QueryLifecycle { closed } -extension DeprecatedQueryLifecycle on QueryLifecycle { - /// No data has been specified from any source - @Deprecated( - 'Use `QueryLifecycle.unexecuted` instead. Will be removed in 5.0.0') - static const UNEXECUTED = QueryLifecycle.unexecuted; - - @Deprecated('Use `QueryLifecycle.pending` instead. Will be removed in 5.0.0') - static QueryLifecycle get PENDING => QueryLifecycle.pending; - - @Deprecated('Use `QueryLifecycle.polling` instead. Will be removed in 5.0.0') - static QueryLifecycle get POLLING => QueryLifecycle.polling; - - @Deprecated( - 'Use `QueryLifecycle.pollingStopped` instead. Will be removed in 5.0.0') - static QueryLifecycle get POLLING_STOPPED => QueryLifecycle.pollingStopped; - - @Deprecated( - 'Use `QueryLifecycle.sideEffectsPending` instead. Will be removed in 5.0.0') - static QueryLifecycle get SIDE_EFFECTS_PENDING => - QueryLifecycle.sideEffectsPending; - - @Deprecated( - 'Use `QueryLifecycle.sideEffectsBlocking` instead. Will be removed in 5.0.0') - static const SIDE_EFFECTS_BLOCKING = QueryLifecycle.sideEffectsBlocking; - - @Deprecated( - 'Use `QueryLifecycle.completed` instead. Will be removed in 5.0.0') - static QueryLifecycle get COMPLETED => QueryLifecycle.completed; - - @Deprecated( - 'Use `QueryLifecycle.completed` instead. Will be removed in 5.0.0') - static QueryLifecycle get CLOSED => QueryLifecycle.closed; -} - /// An Observable/Stream-based API for both queries and mutations. /// Returned from [GraphQLClient.watchQuery] for use in reactive programming, /// for instance in `graphql_flutter` widgets. @@ -148,7 +114,7 @@ class ObservableQuery { case QueryLifecycle.closed: case QueryLifecycle.unexecuted: case QueryLifecycle.sideEffectsPending: - case QueryLifecycle.SIDE_EFFECTS_BLOCKING: + case QueryLifecycle.sideEffectsBlocking: return false; } return false; From 1e893b5debf60e410816496bb795e0cc51132b20 Mon Sep 17 00:00:00 2001 From: micimize Date: Sun, 31 May 2020 12:32:24 -0500 Subject: [PATCH 037/118] feat(graphql): Robust ObservableQuery docs --- .../lib/src/core/observable_query.dart | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 8fc0a7e58..77f4c0159 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -8,6 +8,7 @@ import 'package:graphql/src/core/query_result.dart'; import 'package:graphql/src/core/policies.dart'; import 'package:graphql/src/scheduler/scheduler.dart'; +/// Side effect to register for execution when data is received typedef OnData = void Function(QueryResult result); /// Lifecycle states for [ObservableQuery.lifecycle] @@ -42,8 +43,10 @@ enum QueryLifecycle { } /// An Observable/Stream-based API for both queries and mutations. +/// /// Returned from [GraphQLClient.watchQuery] for use in reactive programming, /// for instance in `graphql_flutter` widgets. +/// It is modelled closely after [Apollo's ObservableQuery][apollo_oq] /// /// [ObservableQuery]'s core api/usage is to [fetchResults], then listen to the [stream]. /// [fetchResults] will be called on instantiation if [options.eagerlyFetchResults] is set, @@ -51,18 +54,11 @@ enum QueryLifecycle { /// /// Beyond that, [ObservableQuery] is a bit of a kitchen sink: /// * There are [refetch] and [fetchMore] methods for fetching more results -/// * [onData] -/// -/// -/// It has -/// -/// Results can be [refetch]ed, -/// -/// It is currently used +/// * An [onData] method for registering callbacks (namely for mutations) /// * [lifecycle] for tracking polling, side effect, an inflight execution state /// * [latestResult] – the most recent result from this operation /// -/// Modelled closely after [Apollo's ObservableQuery][apollo_oq] +/// And a handful of internally leveraged methods. /// /// [apollo_oq]: https://www.apollographql.com/docs/react/v3.0-beta/api/core/ObservableQuery/ class ObservableQuery { @@ -84,8 +80,11 @@ class ObservableQuery { /// The identity of this query within the [QueryManager] final String queryId; + + @protected final QueryManager queryManager; + @protected QueryScheduler get scheduler => queryManager.scheduler; final Set> _onDataSubscriptions = @@ -125,9 +124,15 @@ class ObservableQuery { if (_isRefetchSafe) { return queryManager.refetchQuery(queryId); } - return Future.error(Exception('Query is not refetch safe')); + return Future.error( + Exception('Query is not refetch safe'), + ); } + /// Whether it is safe to rebroadcast results due to cache + /// changes based on [lifecycle]. + /// + /// Called internally by the [QueryManager] bool get isRebroadcastSafe { switch (lifecycle) { case QueryLifecycle.pending: @@ -162,6 +167,9 @@ class ObservableQuery { } } + /// Fetch results based on [options.fetchPolicy] + /// + /// Will [startPolling] if [options.pollInterval] is set MultiSourceResult fetchResults() { final MultiSourceResult allResults = queryManager.fetchQueryAsMultiSourceResult(queryId, options); @@ -199,9 +207,13 @@ class ObservableQuery { ); } - /// add a result to the stream, - /// copying `loading` and `optimistic` - /// from the `latestResult` if they aren't set. + /// Add a [result] to the [stream] unless it was created + /// before [lasestResult]. + /// + /// Copies the [QueryResult.source] from the [latestResult] + /// if it is set to `null`. + /// + /// Called internally by the [QueryManager] void addResult(QueryResult result) { // don't overwrite results due to some async/optimism issue if (latestResult != null && @@ -213,7 +225,7 @@ class ObservableQuery { result.source ??= latestResult.source; } - if (lifecycle == QueryLifecycle.pending && !result.isOptimistic) { + if (lifecycle == QueryLifecycle.pending && result.isConcrete) { lifecycle = QueryLifecycle.completed; } @@ -260,6 +272,9 @@ class ObservableQuery { _onDataSubscriptions.add(subscription); } + /// Poll the server periodically for results. + /// + /// Will be called by [fetchResults] automatically if [options.pollInterval] is set void startPolling(int pollInterval) { if (options.fetchPolicy == FetchPolicy.cacheFirst || options.fetchPolicy == FetchPolicy.cacheOnly) { From 7514b93fa280cd398f73db95b1a86f358bf690d5 Mon Sep 17 00:00:00 2001 From: micimize Date: Mon, 1 Jun 2020 10:56:49 -0500 Subject: [PATCH 038/118] fix(examples): starwars example works again --- .../ios/Runner.xcodeproj/project.pbxproj | 3 - examples/starwars/lib/client_provider.dart | 5 +- examples/starwars/lib/episode/hero_query.dart | 2 +- examples/starwars/lib/main.dart | 2 - examples/starwars/lib/reviews/review.dart | 27 ++++++ .../lib/reviews/review_page_list.dart | 7 +- .../lib/reviews/review_subscription.dart | 96 +++++-------------- examples/starwars/pubspec.yaml | 12 +-- 8 files changed, 61 insertions(+), 93 deletions(-) diff --git a/examples/starwars/ios/Runner.xcodeproj/project.pbxproj b/examples/starwars/ios/Runner.xcodeproj/project.pbxproj index 78fece2c2..526c8e783 100644 --- a/examples/starwars/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/starwars/ios/Runner.xcodeproj/project.pbxproj @@ -315,7 +315,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -387,7 +386,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -441,7 +439,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/examples/starwars/lib/client_provider.dart b/examples/starwars/lib/client_provider.dart index 0ce098b32..98f80bfa7 100644 --- a/examples/starwars/lib/client_provider.dart +++ b/examples/starwars/lib/client_provider.dart @@ -18,13 +18,14 @@ ValueNotifier clientFor({ @required String uri, String subscriptionUri, }) { - Link link = HttpLink(uri: uri); + Link link = HttpLink(uri); if (subscriptionUri != null) { final WebSocketLink websocketLink = WebSocketLink( subscriptionUri, ); - link = link.concat(websocketLink); + link = Link.split((request) => request.isSubscription, websocketLink, link); + ; } return ValueNotifier( diff --git a/examples/starwars/lib/episode/hero_query.dart b/examples/starwars/lib/episode/hero_query.dart index 47cef03fb..77852b7ea 100644 --- a/examples/starwars/lib/episode/hero_query.dart +++ b/examples/starwars/lib/episode/hero_query.dart @@ -40,7 +40,7 @@ class HeroForEpisode extends StatelessWidget { return Text(result.exception.toString()); } - if (result.loading) { + if (result.isLoading) { return const Center( child: CircularProgressIndicator(), ); diff --git a/examples/starwars/lib/main.dart b/examples/starwars/lib/main.dart index 4306e2bbd..c04668935 100644 --- a/examples/starwars/lib/main.dart +++ b/examples/starwars/lib/main.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:universal_platform/universal_platform.dart'; diff --git a/examples/starwars/lib/reviews/review.dart b/examples/starwars/lib/reviews/review.dart index 35680a8dc..86c940b81 100644 --- a/examples/starwars/lib/reviews/review.dart +++ b/examples/starwars/lib/reviews/review.dart @@ -1,4 +1,5 @@ import 'package:meta/meta.dart'; +import 'package:flutter/material.dart'; import '../episode/episode.dart'; @@ -43,3 +44,29 @@ class Review { } const String Function(Object jsonObject) displayReview = getPrettyJSONString; + +class DisplayReviews extends StatelessWidget { + const DisplayReviews({ + Key key, + @required this.reviews, + }) : super(key: key); + + final List> reviews; + + @override + Widget build(BuildContext context) { + return ListView( + padding: const EdgeInsets.all(8.0), + children: reviews + .map(displayReview) + .map((String s) => Card( + child: Container( + padding: const EdgeInsets.all(15.0), + //height: 150, + child: Text(s), + ), + )) + .toList(), + ); + } +} diff --git a/examples/starwars/lib/reviews/review_page_list.dart b/examples/starwars/lib/reviews/review_page_list.dart index 85def029d..40cca69c5 100644 --- a/examples/starwars/lib/reviews/review_page_list.dart +++ b/examples/starwars/lib/reviews/review_page_list.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; -import 'package:starwars_app/reviews/review_subscription.dart' - show DisplayReviews; +import 'package:starwars_app/reviews/review.dart'; class PagingReviews extends StatelessWidget { static const BottomNavigationBarItem navItem = BottomNavigationBarItem( @@ -37,7 +36,7 @@ class PagingReviews extends StatelessWidget { return Text(result.exception.toString()); } - if (result.loading && result.data == null) { + if (result.isLoading && result.data == null) { return const Center( child: CircularProgressIndicator(), ); @@ -53,7 +52,7 @@ class PagingReviews extends StatelessWidget { .cast>(), ), ), - (result.loading) + (result.isLoading) ? Center( child: CircularProgressIndicator(), ) diff --git a/examples/starwars/lib/reviews/review_subscription.dart b/examples/starwars/lib/reviews/review_subscription.dart index a461f2dc4..22d10e22a 100644 --- a/examples/starwars/lib/reviews/review_subscription.dart +++ b/examples/starwars/lib/reviews/review_subscription.dart @@ -6,87 +6,35 @@ import './review.dart'; class ReviewFeed extends StatelessWidget { @override Widget build(BuildContext context) { - return Subscription>( - 'reviewAdded', - r''' - subscription reviewAdded { - reviewAdded { - stars, commentary, episode - } - } - ''', - builder: ({dynamic loading, dynamic payload, dynamic error}) { - if (error != null) { - return Text(error.toString()); + return Subscription( + options: SubscriptionOptions( + document: gql( + r''' + subscription reviewAdded { + reviewAdded { + stars, commentary, episode + } + } + ''', + ), + ), + builder: (result) { + if (result.hasException) { + return Text(result.exception.toString()); } - if (loading == true) { + if (result.isLoading) { return Center( child: const CircularProgressIndicator(), ); } - return ReviewList(newReview: payload as Map); + return ResultAccumulator.appendUniqueEntries( + latest: result.data, + builder: (context, {results}) => DisplayReviews( + reviews: results.reversed.toList(), + ), + ); }, ); } } - -class ReviewList extends StatefulWidget { - const ReviewList({@required this.newReview}); - - final Map newReview; - - @override - _ReviewListState createState() => _ReviewListState(); -} - -class _ReviewListState extends State { - List> reviews; - - @override - void initState() { - reviews = widget.newReview != null ? [widget.newReview] : []; - super.initState(); - } - - @override - void didUpdateWidget(ReviewList oldWidget) { - super.didUpdateWidget(oldWidget); - if (!reviews.contains(widget.newReview)) { - setState(() { - reviews.insert(0, widget.newReview); - }); - } - } - - @override - Widget build(BuildContext context) { - return DisplayReviews(reviews: reviews); - } -} - -class DisplayReviews extends StatelessWidget { - const DisplayReviews({ - Key key, - @required this.reviews, - }) : super(key: key); - - final List> reviews; - - @override - Widget build(BuildContext context) { - return ListView( - padding: const EdgeInsets.all(8.0), - children: reviews - .map(displayReview) - .map((String s) => Card( - child: Container( - padding: const EdgeInsets.all(15.0), - height: 150, - child: Text(s), - ), - )) - .toList(), - ); - } -} diff --git a/examples/starwars/pubspec.yaml b/examples/starwars/pubspec.yaml index 18c1210a4..163930343 100644 --- a/examples/starwars/pubspec.yaml +++ b/examples/starwars/pubspec.yaml @@ -4,23 +4,20 @@ description: An example graphql_flutter application utilizing graphql_starwars_t dependencies: flutter: sdk: flutter - graphql_flutter: + graphql_flutter: path: ../../packages/graphql_flutter graphql: path: ../../packages/graphql universal_platform: ^0.1.3 # https://github.com/flutter/flutter/issues/36126#issuecomment-596215587 - graphql_starwars_test_server: - git: git@github.com:micimize/angel-starwars-test-server.git + graphql_starwars_test_server: ^0.1.0 flutter: uses-material-design: true + + dependency_overrides: - graphql_flutter: - path: ../../packages/graphql_flutter - graphql: - path: ../../packages/graphql graphql_server: git: url: git@github.com:micimize/angel.git @@ -30,3 +27,4 @@ dependency_overrides: git: url: git@github.com:micimize/angel.git path: packages/graphql/graphql_parser + From 36ee5a2d87b32acf797c67df9bc3bf465fc5692a Mon Sep 17 00:00:00 2001 From: micimize Date: Mon, 1 Jun 2020 10:57:59 -0500 Subject: [PATCH 039/118] docs(graphql): add more docstrings and comments --- .../graphql/lib/src/core/_data_class.dart | 11 ++- .../graphql/lib/src/core/query_manager.dart | 11 +-- .../graphql/lib/src/core/query_options.dart | 28 +++++++- packages/graphql/lib/src/graphql_client.dart | 67 +++++++++---------- 4 files changed, 69 insertions(+), 48 deletions(-) diff --git a/packages/graphql/lib/src/core/_data_class.dart b/packages/graphql/lib/src/core/_data_class.dart index 37a51c1fd..744f37a25 100644 --- a/packages/graphql/lib/src/core/_data_class.dart +++ b/packages/graphql/lib/src/core/_data_class.dart @@ -1,7 +1,14 @@ import 'package:meta/meta.dart'; import "package:collection/collection.dart"; -/// Helper for making mutable data clases +/// Helper for making mutable data classes with +/// a [properties] based [equal] helper +/// +/// NOTE: I (@micimize) settled on this helper instead of truly immutable classes +/// because I didn't want to deal with the issue of `copyWith(field: null)`, +/// but also didn't want to commit to adding a true dataclass generator +/// like `freezed` or `built_value` yet. I consider this a stopgap, +/// and think we should eventually have a truly immutable API abstract class MutableDataClass { const MutableDataClass(); @@ -9,7 +16,7 @@ abstract class MutableDataClass { @protected List get properties; - /// [properties] based equality check + /// [properties] based deep equality check bool equal(MutableDataClass other) => identical(this, other) || (runtimeType == other.runtimeType && diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index ed98f6bcf..08fe906be 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -48,6 +48,7 @@ class QueryManager { Stream subscribe(SubscriptionOptions options) async* { final request = options.asRequest; + // Add optimistic or cache-based result to the stream if any if (options.optimisticResult != null) { // TODO optimisticResults for streams just skip the cache for now yield QueryResult.optimistic(data: options.optimisticResult); @@ -92,9 +93,7 @@ class QueryManager { }); } - Future query(QueryOptions options) { - return fetchQuery('0', options); - } + Future query(QueryOptions options) => fetchQuery('0', options); Future mutate(MutationOptions options) { return fetchQuery('0', options).then((result) async { @@ -164,11 +163,7 @@ class QueryManager { try { // execute the request through the provided link(s) - response = await link - .request( - request, - ) - .first; + response = await link.request(request).first; // save the data from response to the cache if (response.data != null && options.fetchPolicy != FetchPolicy.noCache) { diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index dd1af6b79..5b59dfe19 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -98,6 +98,8 @@ class WatchQueryOptions extends QueryOptions { /// Whether or not to fetch results bool fetchResults; + /// Whether to [fetchResults] immediately on instantiation. + /// Defaults to [fetchResults]. bool eagerlyFetchResults; @override @@ -118,17 +120,17 @@ class WatchQueryOptions extends QueryOptions { ); } -/// options for fetchmore operations +/// options for fetchMore operations class FetchMoreOptions { FetchMoreOptions({ - @required this.document, + this.document, this.variables = const {}, @required this.updateQuery, }) : assert(updateQuery != null); DocumentNode document; - final Map variables; + Map variables; /// Strategy for merging the fetchMore result data /// with the result data already in the cache @@ -140,3 +142,23 @@ typedef dynamic UpdateQuery( dynamic previousResultData, dynamic fetchMoreResultData, ); + +extension WithType on Request { + OperationType get type { + final definitions = operation.document.definitions + .whereType() + .toList(); + if (operation.operationName != null) { + definitions.removeWhere( + (node) => node.name.value != operation.operationName, + ); + } + // TODO differentiate error types, add exception + assert(definitions.length == 1); + return definitions.first.type; + } + + bool get isQuery => type == OperationType.query; + bool get isMutation => type == OperationType.mutation; + bool get isSubscription => type == OperationType.subscription; +} diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 3054678c5..f35285832 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -37,46 +37,43 @@ class GraphQLClient { /// based on the provided [WatchQueryOptions]. /// /// {@tool snippet} - /// /// Basic usage - /// ```dart /// - /// result = client.watchQuery(WatchQueryOptions( - /// options: QueryOptions( - /// document: gql(r''' - /// query HeroForEpisode($ep: Episode!) { - /// hero(episode: $ep) { - /// __typename - /// name - /// ... on Droid { - /// primaryFunction - /// } - /// ... on Human { - /// height - /// homePlanet - /// } - /// } - /// } - /// '''), - /// variables: { - /// 'ep': episodeToJson(episode), - /// }, - /// ), - /// )); + /// ```dart + /// final observableQuery = client.watchQuery( + /// WatchQueryOptions( + /// document: gql( + /// r''' + /// query HeroForEpisode($ep: Episode!) { + /// hero(episode: $ep) { + /// name + /// } + /// } + /// ''', + /// ), + /// variables: {'ep': 'NEWHOPE'}, + /// ), + /// ); /// - /// result.stream.listen((QueryResult result) { - /// if (!result.loading && result.data != null) { - /// add( - /// GraphqlLoadedEvent( - /// data: parseData(result.data as Map), - /// result: result, - /// ), - /// ); - /// } - /// if (result.hasException) { - /// add(GraphqlErrorEvent(error: result.exception, result: result)); + /// /// Listen to the stream of results. This will include: + /// /// * `options.optimisitcResult` if passed + /// /// * The result from the server (if `options.fetchPolicy` includes networking) + /// /// * rebroadcast results from edits to the cache + /// observableQuery.stream.listen((QueryResult result) { + /// if (!result.isLoading && result.data != null) { + /// if (result.hasException) { + /// print(result.exception); + /// return; + /// } + /// if (result.isLoading) { + /// print('loading'); + /// return; + /// } + /// doSomethingWithMyQueryResult(myCustomParser(result.data)); /// } /// }); + /// // ... cleanup: + /// observableQuery.close(); /// ``` /// {@end-tool} ObservableQuery watchQuery(WatchQueryOptions options) { From 7e1edeecf753c4d48335088ddb8597b50b1daf08 Mon Sep 17 00:00:00 2001 From: micimize Date: Mon, 1 Jun 2020 10:58:31 -0500 Subject: [PATCH 040/118] feat(graphql_flutter): add ResultAccumulator, fix Subscription --- .../example/lib/graphql_widget/main.dart | 18 ++--- .../graphql_flutter/lib/graphql_flutter.dart | 1 + .../lib/src/widgets/stream_accumulator.dart | 77 +++++++++++++++++++ .../lib/src/widgets/subscription.dart | 55 ++++++++++++- 4 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 packages/graphql_flutter/lib/src/widgets/stream_accumulator.dart diff --git a/packages/graphql_flutter/example/lib/graphql_widget/main.dart b/packages/graphql_flutter/example/lib/graphql_widget/main.dart index 9b85ff1a3..83f647d8b 100644 --- a/packages/graphql_flutter/example/lib/graphql_widget/main.dart +++ b/packages/graphql_flutter/example/lib/graphql_widget/main.dart @@ -114,7 +114,7 @@ class _MyHomePageState extends State { return StarrableRepository( repository: repositories[index], optimistic: result.source == - QueryResultSource.OptimisticResult, + QueryResultSource.optimisticResult, ); }, ), @@ -123,16 +123,14 @@ class _MyHomePageState extends State { ), ), ENABLE_WEBSOCKETS - ? Subscription>( - 'test', queries.testSubscription, builder: ({ - bool loading, - Map payload, - dynamic error, - }) { - return loading + ? Subscription( + options: SubscriptionOptions( + document: gql(queries.testSubscription), + ), + builder: (result) => result.isLoading ? const Text('Loading...') - : Text(payload.toString()); - }) + : Text(result.data.toString()), + ) : const Text(''), ], ), diff --git a/packages/graphql_flutter/lib/graphql_flutter.dart b/packages/graphql_flutter/lib/graphql_flutter.dart index fbb9671ac..375ebdcc6 100644 --- a/packages/graphql_flutter/lib/graphql_flutter.dart +++ b/packages/graphql_flutter/lib/graphql_flutter.dart @@ -8,3 +8,4 @@ export 'package:graphql_flutter/src/widgets/graphql_provider.dart'; export 'package:graphql_flutter/src/widgets/mutation.dart'; export 'package:graphql_flutter/src/widgets/query.dart'; export 'package:graphql_flutter/src/widgets/subscription.dart'; +export 'package:graphql_flutter/src/widgets/stream_accumulator.dart'; diff --git a/packages/graphql_flutter/lib/src/widgets/stream_accumulator.dart b/packages/graphql_flutter/lib/src/widgets/stream_accumulator.dart new file mode 100644 index 000000000..7af6b8582 --- /dev/null +++ b/packages/graphql_flutter/lib/src/widgets/stream_accumulator.dart @@ -0,0 +1,77 @@ +import 'package:meta/meta.dart'; +import 'package:flutter/widgets.dart'; + +/// Same as a fold combine for lists +/// +/// Return `null` to avoid setting state. +typedef Accumulator = List Function( + List previousValue, + Element element, +); + +List _append(List results, T latest) => [...results, latest]; + +List _appendUnique(List results, T latest) { + if (!results.contains(latest)) { + return [...results, latest]; + } + return null; +} + +/// Accumulate stream results into a [List]. +/// +/// Useful for handling [Subscription] results. +class ResultAccumulator extends StatefulWidget { + const ResultAccumulator({ + @required this.latest, + @required this.builder, + Accumulator accumulator, + }) : accumulator = accumulator ?? _append; + + const ResultAccumulator.appendUniqueEntries({ + @required this.latest, + @required this.builder, + }) : accumulator = _appendUnique; + + /// The latest entry in the stream + final T latest; + + /// The strategy for merging entries. Can return `null` + /// to prevent a call to `setState`. + /// + /// Defaults to `(results, latest) => [...results, latest]` + final Accumulator accumulator; + + /// Builds the resulting widget with all accumulated results. + final Widget Function(BuildContext, {@required List results}) builder; + + @override + _ResultAccumulatorState createState() => _ResultAccumulatorState(); +} + +class _ResultAccumulatorState extends State> { + List results; + + @override + void initState() { + results = widget.latest != null ? [widget.latest] : []; + super.initState(); + } + + @override + void didUpdateWidget(ResultAccumulator oldWidget) { + super.didUpdateWidget(oldWidget); + + final newResults = widget.accumulator(results, widget.latest); + + if (newResults != null) { + setState(() { + results = newResults; + }); + } + } + + @override + Widget build(BuildContext context) => + widget.builder(context, results: results); +} diff --git a/packages/graphql_flutter/lib/src/widgets/subscription.dart b/packages/graphql_flutter/lib/src/widgets/subscription.dart index 974de04a4..fbaea9c60 100644 --- a/packages/graphql_flutter/lib/src/widgets/subscription.dart +++ b/packages/graphql_flutter/lib/src/widgets/subscription.dart @@ -14,6 +14,56 @@ typedef OnSubscriptionResult = void Function( typedef SubscriptionBuilder = Widget Function(QueryResult result); +/// Creats a subscription with [GraphQLClient.subscribe]. +/// +/// The [builder] is passed a [QueryResult] with only the **most recent** +/// `data`. [ResultAccumulator] can be used to accumulate results. +/// +/// [onSubscriptionResult] can be used to react to changes, +/// and has access to the `client`. +/// +/// {@tool snippet} +/// +/// Excerpt from the starwars example using [ResultAccumulator] +/// +/// ```dart +/// class ReviewFeed extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Subscription( +/// options: SubscriptionOptions( +/// document: gql( +/// r''' +/// subscription reviewAdded { +/// reviewAdded { +/// stars, commentary, episode +/// } +/// } +/// ''', +/// ), +/// ), +/// builder: (result) { +/// if (result.hasException) { +/// return Text(result.exception.toString()); +/// } +/// +/// if (result.isLoading) { +/// return Center( +/// child: const CircularProgressIndicator(), +/// ); +/// } +/// return ResultAccumulator.appendUniqueEntries( +/// latest: result.data, +/// builder: (context, {results}) => DisplayReviews( +/// reviews: results.reversed.toList(), +/// ), +/// ); +/// }, +/// ); +/// } +/// } +/// ``` +/// {@end-tool} class Subscription extends StatefulWidget { const Subscription({ @required this.options, @@ -38,9 +88,6 @@ class _SubscriptionState extends State> { StreamSubscription _networkSubscription; void _initSubscription() { - final GraphQLClient client = GraphQLProvider.of(context).value; - assert(client != null); - stream = client.subscribe(widget.options); if (widget.onSubscriptionResult != null) { @@ -114,7 +161,7 @@ class _SubscriptionState extends State> { } @override - Widget build(final BuildContext context) { + Widget build(BuildContext context) { return StreamBuilder( initialData: widget.options?.optimisticResult != null ? QueryResult.optimistic(data: widget.options?.optimisticResult) From bc32bddfc37c212538999a9ff2b427b639e454f3 Mon Sep 17 00:00:00 2001 From: micimize Date: Mon, 1 Jun 2020 12:22:03 -0500 Subject: [PATCH 041/118] feat(examples): reorg graphql example so pub displays code --- packages/graphql/example/README.md | 34 +++--- packages/graphql/example/bin/example.dart | 40 +++++++ .../graphql_operation/mutations/addStar.dart | 9 -- .../mutations/mutations.dart | 2 - .../mutations/removeStar.dart | 9 -- .../queries/readRepositories.dart | 23 ---- .../graphql/example/{bin => lib}/main.dart | 113 ++++++++++-------- packages/graphql/example/pubspec.yaml | 3 + 8 files changed, 123 insertions(+), 110 deletions(-) create mode 100644 packages/graphql/example/bin/example.dart delete mode 100644 packages/graphql/example/bin/graphql_operation/mutations/addStar.dart delete mode 100644 packages/graphql/example/bin/graphql_operation/mutations/mutations.dart delete mode 100644 packages/graphql/example/bin/graphql_operation/mutations/removeStar.dart delete mode 100644 packages/graphql/example/bin/graphql_operation/queries/readRepositories.dart rename packages/graphql/example/{bin => lib}/main.dart (55%) diff --git a/packages/graphql/example/README.md b/packages/graphql/example/README.md index 7e5122862..a7907b973 100644 --- a/packages/graphql/example/README.md +++ b/packages/graphql/example/README.md @@ -1,34 +1,30 @@ -# `graphql` Example Application +# `graphql/client.dart` Example Application -This is a simple command line application to showcase how you can use the Dart GraphQL Client, without flutter. +This is a simple command line application to showcase how you can use the Dart GraphQL Client, without flutter. To run this application: +## Setup: + 1. First clone this repository and navigate to this directory 2. Install all dart dependencies -4. create a file `bin/local.dart` with the content: +3. create a file `bin/local.dart` with the content: ```dart const String YOUR_PERSONAL_ACCESS_TOKEN = ''; ``` -3. Then run the Application using the commands below: - - 1. **List Your Repositories** - - ``` - pub run main.dart - ``` - 2. **Star Repository** +## Usage: - ``` - pub run main.dart -a star --id - ``` +```sh +# List repositories +pub run example - 3. **Unstar Repository** +# Star Repository (you can get repository ids from `pub run example`) +pub run example -a star --id $REPOSITORY_ID - ``` - pub run main.dart -a unstar --id - ``` +# Unstar Repository +pub run example -a unstar --id $REPOSITORY_ID +``` -**NB:** Replace repository id in the last two commands with a real Github Repository ID. You can get by running the first command, IDs are printed on the console. +**NB:** Replace repository id in the last two commands with a real Github Repository ID. You can get by running the first command, IDs are printed on the console. diff --git a/packages/graphql/example/bin/example.dart b/packages/graphql/example/bin/example.dart new file mode 100644 index 000000000..83bb34f94 --- /dev/null +++ b/packages/graphql/example/bin/example.dart @@ -0,0 +1,40 @@ +import 'package:args/args.dart'; +import 'package:example/main.dart'; + +ArgResults argResults; + +/// CLI fro executing github actions +/// +/// Usage: +/// ```sh +/// # List repositories +/// pub run example +/// +/// # Star Repository +/// pub run example -a star --id $REPOSITORY_ID_HERE +/// +/// # Unstar Repository +/// pub run example -a unstar --id $REPOSITORY_ID_HERE +/// ``` +void main(List arguments) { + final ArgParser parser = ArgParser() + ..addOption('action', abbr: 'a', defaultsTo: 'fetch') + ..addOption('id', defaultsTo: ''); + + argResults = parser.parse(arguments); + + final String action = argResults['action'] as String; + final String id = argResults['id'] as String; + + switch (action) { + case 'star': + starRepository(id); + break; + case 'unstar': + removeStarFromRepository(id); + break; + default: + readRepositories(); + break; + } +} diff --git a/packages/graphql/example/bin/graphql_operation/mutations/addStar.dart b/packages/graphql/example/bin/graphql_operation/mutations/addStar.dart deleted file mode 100644 index aabb79004..000000000 --- a/packages/graphql/example/bin/graphql_operation/mutations/addStar.dart +++ /dev/null @@ -1,9 +0,0 @@ -const String addStar = r''' - mutation AddStar($starrableId: ID!) { - action: addStar(input: {starrableId: $starrableId}) { - starrable { - viewerHasStarred - } - } - } -'''; diff --git a/packages/graphql/example/bin/graphql_operation/mutations/mutations.dart b/packages/graphql/example/bin/graphql_operation/mutations/mutations.dart deleted file mode 100644 index 19ec2b51e..000000000 --- a/packages/graphql/example/bin/graphql_operation/mutations/mutations.dart +++ /dev/null @@ -1,2 +0,0 @@ -export './addStar.dart'; -export './removeStar.dart'; diff --git a/packages/graphql/example/bin/graphql_operation/mutations/removeStar.dart b/packages/graphql/example/bin/graphql_operation/mutations/removeStar.dart deleted file mode 100644 index 69f33295b..000000000 --- a/packages/graphql/example/bin/graphql_operation/mutations/removeStar.dart +++ /dev/null @@ -1,9 +0,0 @@ -const String removeStar = r''' - mutation RemoveStar($starrableId: ID!) { - action: removeStar(input: {starrableId: $starrableId}) { - starrable { - viewerHasStarred - } - } - } -'''; diff --git a/packages/graphql/example/bin/graphql_operation/queries/readRepositories.dart b/packages/graphql/example/bin/graphql_operation/queries/readRepositories.dart deleted file mode 100644 index 8eceea119..000000000 --- a/packages/graphql/example/bin/graphql_operation/queries/readRepositories.dart +++ /dev/null @@ -1,23 +0,0 @@ -const String readRepositories = r''' - query ReadRepositories($nRepositories: Int!) { - viewer { - repositories(last: $nRepositories) { - nodes { - __typename - id - name - viewerHasStarred - } - } - } - } -'''; - -const String testSubscription = r''' - subscription test { - deviceChanged(id: 2) { - id - name - } - } -'''; diff --git a/packages/graphql/example/bin/main.dart b/packages/graphql/example/lib/main.dart similarity index 55% rename from packages/graphql/example/bin/main.dart rename to packages/graphql/example/lib/main.dart index 8e4ff8919..7ca7e07c8 100644 --- a/packages/graphql/example/bin/main.dart +++ b/packages/graphql/example/lib/main.dart @@ -1,26 +1,31 @@ +/// Example functions for calling the Github GraphQL API +/// +/// ### Queries +/// * [readRepositories()] +/// +/// ### Mutations: +/// * [starRepository(id)] +/// * [removeStarFromRepository(id)] +/// +/// To run the example, create a file `lib/local.dart` with the content: +/// ```dart +/// const String YOUR_PERSONAL_ACCESS_TOKEN = +/// ''; +/// ``` import 'dart:io' show stdout, stderr, exit; - -import 'package:args/args.dart'; import 'package:graphql/client.dart'; -import './graphql_operation/mutations/mutations.dart'; -import './graphql_operation/queries/readRepositories.dart'; - -// to run the example, create a file ../local.dart with the content: -// const String YOUR_PERSONAL_ACCESS_TOKEN = -// ''; // ignore: uri_does_not_exist import './local.dart'; -ArgResults argResults; - -// client - create a graphql client -GraphQLClient client() { - /// `graphql/client.dart` leverages the [gql_link][1] interface, - /// re-exporting `HttpLink`, `WebsocketLink`, `ErrorLink`, and `DedupeLink`, - /// in addition to the links we define ourselves (`AuthLink`) - /// - /// [1]: https://pub.dev/packages/gql_link +/// Get an authenticated [GraphQLClient] for the github api +/// +/// `graphql/client.dart` leverages the [gql_link][1] interface, +/// re-exporting [HttpLink], [WebsocketLink], [ErrorLink], and [DedupeLink], +/// in addition to the links we define ourselves (`AuthLink`) +/// +/// [1]: https://pub.dev/packages/gql_link +GraphQLClient getGithubGraphQLClient() { final Link _link = HttpLink( 'https://api.github.com/graphql', defaultHeaders: { @@ -34,14 +39,29 @@ GraphQLClient client() { ); } -// query example - fetch all your github repositories -void query() async { - final GraphQLClient _client = client(); +/// query example - fetch all your github repositories +void readRepositories() async { + final GraphQLClient _client = getGithubGraphQLClient(); const int nRepositories = 50; final QueryOptions options = QueryOptions( - document: gql(readRepositories), + document: gql( + r''' + query ReadRepositories($nRepositories: Int!) { + viewer { + repositories(last: $nRepositories) { + nodes { + __typename + id + name + viewerHasStarred + } + } + } + } + ''', + ), variables: { 'nRepositories': nRepositories, }, @@ -71,10 +91,20 @@ void starRepository(String repositoryID) async { exit(2); } - final GraphQLClient _client = client(); + final GraphQLClient _client = getGithubGraphQLClient(); final MutationOptions options = MutationOptions( - document: gql(addStar), + document: gql( + r''' + mutation AddStar($starrableId: ID!) { + action: addStar(input: {starrableId: $starrableId}) { + starrable { + viewerHasStarred + } + } + } + ''', + ), variables: { 'starrableId': repositoryID, }, @@ -104,10 +134,20 @@ void removeStarFromRepository(String repositoryID) async { exit(2); } - final GraphQLClient _client = client(); + final GraphQLClient _client = getGithubGraphQLClient(); final MutationOptions options = MutationOptions( - document: gql(removeStar), + document: gql( + r''' + mutation RemoveStar($starrableId: ID!) { + action: removeStar(input: {starrableId: $starrableId}) { + starrable { + viewerHasStarred + } + } + } + ''', + ), variables: { 'starrableId': repositoryID, }, @@ -129,26 +169,3 @@ void removeStarFromRepository(String repositoryID) async { exit(0); } - -void main(List arguments) { - final ArgParser parser = ArgParser() - ..addOption('action', abbr: 'a', defaultsTo: 'fetch') - ..addOption('id', defaultsTo: ''); - - argResults = parser.parse(arguments); - - final String action = argResults['action'] as String; - final String id = argResults['id'] as String; - - switch (action) { - case 'star': - starRepository(id); - break; - case 'unstar': - removeStarFromRepository(id); - break; - default: - query(); - break; - } -} diff --git a/packages/graphql/example/pubspec.yaml b/packages/graphql/example/pubspec.yaml index e8064a72b..fa4674a10 100644 --- a/packages/graphql/example/pubspec.yaml +++ b/packages/graphql/example/pubspec.yaml @@ -9,3 +9,6 @@ dependencies: args: graphql: path: .. + +executables: + example: example From 3246394dcdba7cb8cdc8159265d07eda60cf9153 Mon Sep 17 00:00:00 2001 From: micimize Date: Mon, 1 Jun 2020 12:22:45 -0500 Subject: [PATCH 042/118] refactor(graphql_flutter): rename result accumulator --- packages/graphql_flutter/lib/graphql_flutter.dart | 2 +- .../{stream_accumulator.dart => result_accumulator.dart} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/graphql_flutter/lib/src/widgets/{stream_accumulator.dart => result_accumulator.dart} (100%) diff --git a/packages/graphql_flutter/lib/graphql_flutter.dart b/packages/graphql_flutter/lib/graphql_flutter.dart index 375ebdcc6..108db0943 100644 --- a/packages/graphql_flutter/lib/graphql_flutter.dart +++ b/packages/graphql_flutter/lib/graphql_flutter.dart @@ -8,4 +8,4 @@ export 'package:graphql_flutter/src/widgets/graphql_provider.dart'; export 'package:graphql_flutter/src/widgets/mutation.dart'; export 'package:graphql_flutter/src/widgets/query.dart'; export 'package:graphql_flutter/src/widgets/subscription.dart'; -export 'package:graphql_flutter/src/widgets/stream_accumulator.dart'; +export 'package:graphql_flutter/src/widgets/result_accumulator.dart'; diff --git a/packages/graphql_flutter/lib/src/widgets/stream_accumulator.dart b/packages/graphql_flutter/lib/src/widgets/result_accumulator.dart similarity index 100% rename from packages/graphql_flutter/lib/src/widgets/stream_accumulator.dart rename to packages/graphql_flutter/lib/src/widgets/result_accumulator.dart From 6ba687ec90c336cc47394230f95abdce80aa1392 Mon Sep 17 00:00:00 2001 From: micimize Date: Mon, 1 Jun 2020 12:41:01 -0500 Subject: [PATCH 043/118] fix(graphql): default-yet-overrideable variable --- packages/graphql/lib/src/core/_base_options.dart | 2 +- .../graphql/lib/src/core/mutation_options.dart | 2 +- packages/graphql/lib/src/core/query_options.dart | 8 ++++---- packages/graphql/pubspec.yaml | 14 ++++++-------- .../graphql/test/anonymous_operations_test.dart | 2 +- packages/graphql/test/graphql_client_test.dart | 4 ++-- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/graphql/lib/src/core/_base_options.dart b/packages/graphql/lib/src/core/_base_options.dart index 73a96345e..c753eb16b 100644 --- a/packages/graphql/lib/src/core/_base_options.dart +++ b/packages/graphql/lib/src/core/_base_options.dart @@ -11,8 +11,8 @@ import 'package:graphql/src/core/policies.dart'; abstract class BaseOptions extends MutableDataClass { BaseOptions({ @required this.document, + this.variables = const {}, this.operationName, - this.variables, Context context, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, diff --git a/packages/graphql/lib/src/core/mutation_options.dart b/packages/graphql/lib/src/core/mutation_options.dart index d7ef6c28c..29e3c5786 100644 --- a/packages/graphql/lib/src/core/mutation_options.dart +++ b/packages/graphql/lib/src/core/mutation_options.dart @@ -22,7 +22,7 @@ class MutationOptions extends BaseOptions { MutationOptions({ @required DocumentNode document, String operationName, - Map variables, + Map variables = const {}, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, Context context, diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index 5b59dfe19..8d4db98bb 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -12,7 +12,7 @@ class QueryOptions extends BaseOptions { QueryOptions({ @required DocumentNode document, String operationName, - Map variables, + Map variables = const {}, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, Object optimisticResult, @@ -52,7 +52,7 @@ class SubscriptionOptions extends BaseOptions { SubscriptionOptions({ @required DocumentNode document, String operationName, - Map variables, + Map variables = const {}, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, Object optimisticResult, @@ -75,7 +75,7 @@ class WatchQueryOptions extends QueryOptions { WatchQueryOptions({ @required DocumentNode document, String operationName, - Map variables, + Map variables = const {}, FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, Object optimisticResult, @@ -124,7 +124,7 @@ class WatchQueryOptions extends QueryOptions { class FetchMoreOptions { FetchMoreOptions({ this.document, - this.variables = const {}, + this.variables = const {}, @required this.updateQuery, }) : assert(updateQuery != null); diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index c08ed7df9..63b1ac973 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,16 +2,12 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 3.1.0-beta.4 -authors: - - Eus Dima - - Zino Hofmann - - Michael Joseph Rosenthal - - TruongSinh Tran-Nguyen +version: 4.0.0-alpha.0 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 path: ^1.6.2 + gql: ^0.12.0 gql_exec: ^0.2.2 gql_link: ^0.3.0 @@ -22,15 +18,17 @@ dependencies: gql_dedupe_link: ^1.0.10 hive: ^1.3.0 - - quiver: ">=2.0.0 <3.0.0" normalize: ^0.2.0 + http: ^0.12.1 + collection: ^1.14.12 + dev_dependencies: pedantic: ^1.8.0+1 mockito: ^4.0.0 test: ^1.5.3 test_coverage: ^0.3.0+1 + environment: sdk: ">=2.6.0 <3.0.0" diff --git a/packages/graphql/test/anonymous_operations_test.dart b/packages/graphql/test/anonymous_operations_test.dart index e69536cbe..8a5b2687f 100644 --- a/packages/graphql/test/anonymous_operations_test.dart +++ b/packages/graphql/test/anonymous_operations_test.dart @@ -158,7 +158,7 @@ void main() { operation: Operation( document: parseString(addStar), ), - variables: {}, + variables: {}, context: Context(), ), ), diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index b8578b69c..4bb612b49 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -102,7 +102,7 @@ void main() { Request( operation: Operation( document: parseString(readRepositories), - operationName: 'ReadRepositories', + //operationName: 'ReadRepositories', ), variables: { 'nRepositories': 42, @@ -208,7 +208,7 @@ void main() { Request( operation: Operation( document: parseString(addStar), - operationName: 'AddStar', + //operationName: 'AddStar', ), variables: {}, context: Context(), From 9a5fff1192bd8af069dd8d2ce8723a2598c13341 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 4 Jun 2020 13:52:28 -0500 Subject: [PATCH 044/118] fix(graphql): fix rebroadcasting by refactoring onData callbacks into a simpler async function --- examples/flutter_bloc/test/bloc_test.dart | 2 +- packages/graphql/README.md | 9 +- packages/graphql/lib/src/cache/cache.dart | 11 ++- .../lib/src/core/observable_query.dart | 81 +++++++++++------- .../graphql/lib/src/core/query_manager.dart | 16 ++-- .../lib/src/exceptions/exceptions.dart | 3 + packages/graphql/lib/src/graphql_client.dart | 82 ++++++++++++++++++- .../example/lib/graphql_widget/main.dart | 38 +++++---- 8 files changed, 182 insertions(+), 60 deletions(-) diff --git a/examples/flutter_bloc/test/bloc_test.dart b/examples/flutter_bloc/test/bloc_test.dart index 1f87e17f8..c7c74e4ac 100644 --- a/examples/flutter_bloc/test/bloc_test.dart +++ b/examples/flutter_bloc/test/bloc_test.dart @@ -77,7 +77,7 @@ void main() { final results = QueryResult( data: decodeGithubResponse['data'], exception: null, - loading: false, + source: QueryResultSource.network, ); when( diff --git a/packages/graphql/README.md b/packages/graphql/README.md index 9d7e5e68a..c007c0c57 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -18,7 +18,7 @@ First, depend on this package: ```yaml dependencies: - graphql: ^4.0.0-rc1 + graphql: ^4.0.0-alpha ``` And then import it inside your dart code: @@ -29,7 +29,7 @@ import 'package:graphql/client.dart'; ## Migration Guide -Find the migration from version 2 to version 3 [here](./../../changelog-v2-v3.md). +Find the migration from version 3 to version 4 [here](./../../changelog-v3-v4.md). ### Parsing at build-time @@ -61,7 +61,10 @@ final AuthLink _authLink = AuthLink( final Link _link = _authLink.concat(_httpLink); final GraphQLClient _client = GraphQLClient( - cache: GraphQLCache(), + cache: GraphQLCache( + // The default store is the InMemoryStore, which does NOT persist to disk + store: HiveStore(), + ), link: _link, ); diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index 04b52942d..88fc73ada 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -11,6 +11,11 @@ export 'package:graphql/src/cache/data_proxy.dart'; export 'package:graphql/src/cache/store.dart'; export 'package:graphql/src/cache/hive_store.dart'; +/// Optimmistic GraphQL Entity cache with [normalize] [TypePolicy] support +/// and configurable [store]. +/// +/// **NOTE**: The default [InMemoryStore] does _not_ persist to disk. +/// The recommended store for persistent environments is the [HiveStore]. class GraphQLCache extends NormalizingDataProxy { GraphQLCache({ Store store, @@ -18,12 +23,14 @@ class GraphQLCache extends NormalizingDataProxy { this.typePolicies = const {}, }) : store = store ?? InMemoryStore(); - /// Stores the underlying normalized data + /// Stores the underlying normalized data. Defaults to an [InMemoryStore] @protected final Store store; - /// `typePolicies` to pass down to `normalize` + /// `typePolicies` to pass down to [normalize] final Map typePolicies; + + /// Optional `dataIdFromObject` function to pass through to [normalize] final DataIdResolver dataIdFromObject; /// tracks the number of ongoing transactions to prevent diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 77f4c0159..85d8fdf4a 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:graphql/client.dart'; import 'package:meta/meta.dart'; import 'package:graphql/src/core/query_manager.dart'; @@ -87,8 +88,14 @@ class ObservableQuery { @protected QueryScheduler get scheduler => queryManager.scheduler; - final Set> _onDataSubscriptions = - >{}; + /// callbacks registered with [onData] + List _onDataCallbacks = []; + + /// call [queryManager.maybeRebroadcastQueries] after all other [_onDataCallbacks] + /// + /// Automatically appended as an [OnData] + void _maybeRebroadcast(QueryResult result) => + queryManager.maybeRebroadcastQueries(exclude: this); /// The most recently seen result from this operation's stream QueryResult latestResult; @@ -177,7 +184,7 @@ class ObservableQuery { // if onData callbacks have been registered, // they are waited on by default - lifecycle = _onDataSubscriptions.isNotEmpty + lifecycle = _onDataCallbacks.isNotEmpty ? QueryLifecycle.sideEffectsPending : QueryLifecycle.pending; @@ -214,7 +221,7 @@ class ObservableQuery { /// if it is set to `null`. /// /// Called internally by the [QueryManager] - void addResult(QueryResult result) { + void addResult(QueryResult result, {bool fromRebroadcast = false}) { // don't overwrite results due to some async/optimism issue if (latestResult != null && latestResult.timestamp.isAfter(result.timestamp)) { @@ -231,9 +238,14 @@ class ObservableQuery { latestResult = result; + // TODO should callbacks be applied before or after streaming if (!controller.isClosed) { controller.add(result); } + + if (result.isNotLoading) { + _applyCallbacks(result, fromRebroadcast: fromRebroadcast); + } } // most mutation behavior happens here @@ -245,31 +257,44 @@ class ObservableQuery { /// handling the resolution of [lifecycle] from /// [QueryLifecycle.sideEffectsBlocking] to [QueryLifecycle.completed] /// as appropriate - void onData(Iterable callbacks) { - callbacks ??= const []; - StreamSubscription subscription; - - subscription = stream.where((result) => result.isNotLoading).listen( - (QueryResult result) async { - for (final callback in callbacks) { - await callback(result); - } + void onData(Iterable callbacks) => + _onDataCallbacks.addAll(callbacks ?? []); - if (result.isConcrete) { - await subscription.cancel(); - _onDataSubscriptions.remove(subscription); + /// Applies [onData] callbacks at the end of [addResult] + /// + /// [fromRebroadcast] is used to avoid the super-edge case of infinite rebroadcasts + /// (not sure if it's even possible) + void _applyCallbacks(QueryResult result, + {bool fromRebroadcast = false}) async { + final callbacks = [ + ..._onDataCallbacks, + if (!fromRebroadcast) _maybeRebroadcast + ]; + for (final callback in callbacks) { + await callback(result); + } - if (_onDataSubscriptions.isEmpty) { - if (lifecycle == QueryLifecycle.sideEffectsBlocking) { - lifecycle = QueryLifecycle.completed; - close(); - } - } - } - }, - ); + if (this == null || lifecycle == QueryLifecycle.closed) { + // .close(force: true) was called + return; + } + + if (result.isConcrete) { + // avoid removing new callbacks + _onDataCallbacks.removeWhere((cb) => callbacks.contains(cb)); - _onDataSubscriptions.add(subscription); + // if there are new callbacks, there is maybe another inflight mutation + if (_onDataCallbacks.isEmpty) { + if (lifecycle == QueryLifecycle.sideEffectsBlocking) { + lifecycle = QueryLifecycle.completed; + close(); + } + if (lifecycle == QueryLifecycle.sideEffectsPending) { + lifecycle = QueryLifecycle.completed; + close(); + } + } + } } /// Poll the server periodically for results. @@ -333,10 +358,6 @@ class ObservableQuery { queryManager.closeQuery(this, fromQuery: true); } - for (StreamSubscription subscription in _onDataSubscriptions) { - await subscription.cancel(); - } - stopPolling(); await controller.close(); diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 08fe906be..f0f16375d 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -161,13 +161,15 @@ class QueryManager { Response response; QueryResult queryResult; + final writeToCache = options.fetchPolicy != FetchPolicy.noCache; + try { // execute the request through the provided link(s) response = await link.request(request).first; // save the data from response to the cache - if (response.data != null && options.fetchPolicy != FetchPolicy.noCache) { - cache.writeQuery(request, data: response.data); + if (response.data != null && writeToCache) { + await cache.writeQuery(request, data: response.data); } queryResult = mapFetchResultToQueryResult( @@ -188,7 +190,7 @@ class QueryManager { // cleanup optimistic results cache.removeOptimisticPatch(queryId); - if (options.fetchPolicy != FetchPolicy.noCache) { + if (writeToCache) { // normalize results if previously written queryResult.data = cache.readQuery(request); } @@ -275,7 +277,7 @@ class QueryManager { } /// Add a result to the [ObservableQuery] specified by `queryId`, if it exists - /// Will [maybeRebroadcastQueries] if the cache has flagged the need to + /// Will [maybeRebroadcastQueries] from [addResult] if the cache has flagged the need to /// /// Queries are registered via [setQuery] and [watchQuery] void addQueryResult( @@ -296,8 +298,6 @@ class QueryManager { if (observableQuery != null && !observableQuery.controller.isClosed) { observableQuery.addResult(queryResult); } - - maybeRebroadcastQueries(exclude: observableQuery); } /// Create an optimstic result for the query specified by `queryId`, if it exists @@ -330,9 +330,11 @@ class QueryManager { /// If there are multiple in-flight cache updates, we wait until they all complete bool maybeRebroadcastQueries({ObservableQuery exclude}) { final shouldBroadast = cache.shouldBroadcast(claimExecution: true); + if (!shouldBroadast) { return false; } + for (ObservableQuery query in queries.values) { if (query != exclude && query.isRebroadcastSafe) { final dynamic cachedData = cache.readQuery( @@ -344,9 +346,9 @@ class QueryManager { mapFetchResultToQueryResult( Response(data: cachedData), query.options, - // TODO maybe entirely wrong source: QueryResultSource.cache, ), + fromRebroadcast: true, ); } } diff --git a/packages/graphql/lib/src/exceptions/exceptions.dart b/packages/graphql/lib/src/exceptions/exceptions.dart index f7e9c6d88..d361e1342 100644 --- a/packages/graphql/lib/src/exceptions/exceptions.dart +++ b/packages/graphql/lib/src/exceptions/exceptions.dart @@ -8,6 +8,9 @@ import 'package:graphql/src/exceptions/network.dart' if (dart.library.io) 'package:graphql/src/exceptions/network_io.dart' as network; +export 'package:graphql/src/exceptions/network.dart' + if (dart.library.io) 'package:graphql/src/exceptions/network_io.dart'; + LinkException translateFailure(dynamic failure) { if (failure is LinkException) { return failure; diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index f35285832..1c369aea5 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -6,8 +6,15 @@ import 'package:graphql/src/cache/cache.dart'; import 'package:graphql/src/core/fetch_more.dart'; +/// Universal GraphQL Client with configurable caching and [link][] system. +/// modelled after the [`apollo-client`][ac]. +/// /// The link is a [Link] over which GraphQL documents will be resolved into a [Response]. -/// The cache is the initial [Cache] to use in the data store. +/// The cache is the [GraphQLCache] to use for caching results and optimistic updates. +/// +/// +/// [ac]: https://www.apollographql.com/docs/react/v3.0-beta/api/core/ApolloClient/ +/// [link]: https://github.com/gql-dart/gql/tree/master/links/gql_link class GraphQLClient { /// Constructs a [GraphQLClient] given a [Link] and a [Cache]. GraphQLClient({ @@ -84,6 +91,44 @@ class GraphQLClient { /// This resolves a single query according to the [QueryOptions] specified and /// returns a [Future] which resolves with the [QueryResult] or throws an [Exception]. + /// + /// {@tool snippet} + /// Basic usage + /// + /// ```dart + /// final QueryResult result = await client.query( + /// QueryOptions( + /// document: gql( + /// r''' + /// query ReadRepositories($nRepositories: Int!) { + /// viewer { + /// repositories(last: $nRepositories) { + /// nodes { + /// __typename + /// id + /// name + /// viewerHasStarred + /// } + /// } + /// } + /// } + /// ''', + /// ), + /// variables: { + /// 'nRepositories': 50, + /// }, + /// ), + /// ); + /// + /// if (result.hasException) { + /// print(result.exception.toString()); + /// } + /// + /// final List repositories = + /// result.data['viewer']['repositories']['nodes'] as List; + /// ``` + /// {@end-tool} + Future query(QueryOptions options) { options.policies = defaultPolicies.query.withOverrides(options.policies); return queryManager.query(options); @@ -98,6 +143,40 @@ class GraphQLClient { /// This subscribes to a GraphQL subscription according to the options specified and returns a /// [Stream] which either emits received data or an error. + /// + /// {@tool snippet} + /// Basic usage + /// + /// ```dart + /// subscription = client.subscribe( + /// SubscriptionOptions( + /// document: gql( + /// r''' + /// subscription reviewAdded { + /// reviewAdded { + /// stars, commentary, episode + /// } + /// } + /// ''', + /// ), + /// ), + /// ); + /// + /// subscription.listen((result) { + /// if (result.hasException) { + /// print(result.exception.toString()); + /// return; + /// } + /// + /// if (result.isLoading) { + /// print('awaiting results'); + /// return; + /// } + /// + /// print('Rew Review: ${result.data}'); + /// }); + /// ``` + /// {@end-tool} Stream subscribe(SubscriptionOptions options) { options.policies = defaultPolicies.subscribe.withOverrides( options.policies, @@ -107,6 +186,7 @@ class GraphQLClient { /// Fetch more results and then merge them with the given [previousResult] /// according to [FetchMoreOptions.updateQuery]. + @experimental Future fetchMore( FetchMoreOptions fetchMoreOptions, { @required QueryOptions originalOptions, diff --git a/packages/graphql_flutter/example/lib/graphql_widget/main.dart b/packages/graphql_flutter/example/lib/graphql_widget/main.dart index 83f647d8b..7a32e4e14 100644 --- a/packages/graphql_flutter/example/lib/graphql_widget/main.dart +++ b/packages/graphql_flutter/example/lib/graphql_widget/main.dart @@ -91,7 +91,7 @@ class _MyHomePageState extends State { Query( options: QueryOptions( document: gql(queries.readRepositories), - variables: { + variables: { 'nRepositories': nRepositories, }, //pollInterval: 10, @@ -149,9 +149,8 @@ class StarrableRepository extends StatelessWidget { final Map repository; final bool optimistic; - Map extractRepositoryData(Object data) { - final action = - (data as Map)['action'] as Map; + Map extractRepositoryData(Map data) { + final action = data['action'] as Map; if (action == null) { return null; } @@ -161,8 +160,13 @@ class StarrableRepository extends StatelessWidget { bool get starred => repository['viewerHasStarred'] as bool; Map get expectedResult => { - 'action': { - 'starrable': {'viewerHasStarred': !starred} + 'action': { + '__typename': 'AddStarPayload', + 'starrable': { + '__typename': 'Repository', + 'id': repository['id'], + 'viewerHasStarred': !starred, + } } }; @@ -171,26 +175,30 @@ class StarrableRepository extends StatelessWidget { return Mutation( options: MutationOptions( document: gql(starred ? mutations.removeStar : mutations.addStar), - update: (cache, QueryResult result) { + update: (cache, result) { if (result.hasException) { print(result.exception); } else { - final updated = Map.from(repository) - ..addAll(extractRepositoryData(result.data)); + final updated = { + ...repository, + ...extractRepositoryData(result.data), + }; cache.writeFragment( - fragment: gql(''' + fragment: gql( + ''' fragment fields on Repository { - __typename id name viewerHasStarred } - '''), + ''', + ), idFields: { '__typename': updated['__typename'], 'id': updated['id'], }, data: updated, + broadcast: false, ); } }, @@ -243,15 +251,13 @@ class StarrableRepository extends StatelessWidget { color: Colors.amber, ) : const Icon(Icons.star_border), - trailing: result.loading || optimistic + trailing: result.isLoading || optimistic ? const CircularProgressIndicator() : null, title: Text(repository['name'] as String), onTap: () { toggleStar( - { - 'starrableId': repository['id'], - }, + {'starrableId': repository['id']}, optimisticResult: expectedResult, ); }, From 20d017612072db9563842a4ba2322c8b3101ab3a Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 5 Jun 2020 11:29:00 -0500 Subject: [PATCH 045/118] feat(graphql): re-add documentNode asdeprecated --- changelog-v3-v4.md | 136 ++++++++++++++++++ examples/starwars/lib/client_provider.dart | 1 - .../lib/src/core/mutation_options.dart | 11 +- .../lib/src/core/observable_query.dart | 2 +- .../graphql/lib/src/core/query_options.dart | 25 +++- .../example/lib/graphql_widget/main.dart | 6 +- v4_devnotes.md | 40 ------ 7 files changed, 167 insertions(+), 54 deletions(-) create mode 100644 changelog-v3-v4.md delete mode 100644 v4_devnotes.md diff --git a/changelog-v3-v4.md b/changelog-v3-v4.md new file mode 100644 index 000000000..beb15f227 --- /dev/null +++ b/changelog-v3-v4.md @@ -0,0 +1,136 @@ +# Migrating from v3 – v4 + +v4 aims to solve a number of sore spots, particularly with caching, largely by leveraging libraries from the https://github.com/gql-dart ecosystem. There has also been a concerted effort to add more API docstrings to the codebase. + +## Cache overhaul +* There is now only a single `GraphQLCache`, which leverages [normalize](https://pub.dev/packages/normalize), + Giving us a much more `apollo`ish api including `typePolicies` +* `LazyCacheMap` has been deleted +* `GraphQLCache` marks itself for rebroadcasting (should fix some related issues) +* **`Store`** is now a seperate concern: + +```dart +GraphQLCache( + // The default store is the InMemoryStore, which does NOT persist to disk + store: HiveStore(), +) +``` + +and persistence is broken into a seperate `Store` concern. + +## We now use the [gql_link system](https://github.com/gql-dart/gql/tree/master/links/gql_link) +* Most links are re-exported from `graphql/client.dart` +* `QueryOptions`, `MutationOptions`, etc are turned into +[gql_exec](https://github.com/gql-dart/gql/tree/master/links/gql_exec) `Request`s +before being sent down the link chain. +* `documentNode` is deprecated in favor of `DocumentNode document` for consistency with `gql` libraries +* We won't leave alpha until we have [full backwards compatability](https://github.com/gql-dart/gql/issues/57) + +```diff +final httpLink = HttpLink( +- uri: 'https://api.github.com/graphql', ++ 'https://api.github.com/graphql', +); + +final authLink = AuthLink( + getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', +); + +var link = authLink.concat(httpLink); + +if (ENABLE_WEBSOCKETS) { + final websocketLink = WebSocketLink( +- uri: 'ws://localhost:8080/ws/graphql' ++ 'ws://localhost:8080/ws/graphql' + ); + +- link = link.concat(websocketLink); ++ // split request based on type ++ link = Link.split( ++ (request) => request.isSubscription, ++ websocketLink, ++ link, ++ ); +} +``` + +This makes all link development coordinated across the ecosystem, so that we can leverage existing links like [gql_dio_link](https://pub.dev/packages/gql_dio_link), and all link-based clients benefit from new link development + +## `subscription` API overhaul + +`Subscription`/`client.subscribe` API is in line with the rest of the API + +```dart +final subscriptionDocument = gql( + r''' + subscription reviewAdded { + reviewAdded { + stars, commentary, episode + } + } + ''', +); +// graphql/client.dart usage +subscription = client.subscribe( + SubscriptionOptions( + document: subscriptionDocument + ), +); + +// graphql_flutter/graphql_flutter.dart usage +Subscription( + options: SubscriptionOptions( + document: subscriptionDocument, + ), + builder: (result) { /*...*/ }, +); +``` + +## Minor changes + +* As mentioned before, `documentNode: gql(...)` is now `document: gql(...)`. +* The exported `gql` utility adds `__typename` automatically. + **If you define your own, make sure to include `AddTypenameVisitor`, + or else that your cache `dataIdFromObject` works without it + +### Enums are normalized and idiomatic + +```diff +- QueryResultSource.OptimisticResult ++ QueryResultSource.optimisticResult +- QueryResultSource.Cache ++ QueryResultSource.cache +// etc + +- QueryLifecycle.UNEXECUTED ++ QueryLifecycle.unexecuted +- QueryLifecycle.SIDE_EFFECTS_PENDING ++ QueryLifecycle.sideEffectsPending +``` + + +### `client.fetchMore` (experimental) + +The `fetchMore` logic is now available for when one isn't using `watchQuery`: +```dart +/// Untested example code +class MyQuery { + QueryResult latestResult; + QueryOptions initialOptions; + + FetchMoreOptions get _fetchMoreOptions { + // resolve the fetchMore params based on some data in lastestResult, + // like last item id or page number, and provide custom updateQuery logic + } + + Future fetchMore() async { + final result = await client.fetchMore( + _fetchMoreOptions, + options: options, + previousResult: latestResult, + ); + _latestResult = result; + return result; + } +} +``` diff --git a/examples/starwars/lib/client_provider.dart b/examples/starwars/lib/client_provider.dart index 98f80bfa7..977f6a62b 100644 --- a/examples/starwars/lib/client_provider.dart +++ b/examples/starwars/lib/client_provider.dart @@ -25,7 +25,6 @@ ValueNotifier clientFor({ ); link = Link.split((request) => request.isSubscription, websocketLink, link); - ; } return ValueNotifier( diff --git a/packages/graphql/lib/src/core/mutation_options.dart b/packages/graphql/lib/src/core/mutation_options.dart index 29e3c5786..e86e87b34 100644 --- a/packages/graphql/lib/src/core/mutation_options.dart +++ b/packages/graphql/lib/src/core/mutation_options.dart @@ -1,7 +1,7 @@ +// ignore_for_file: deprecated_member_use_from_same_package import 'package:graphql/src/cache/cache.dart'; import 'package:graphql/src/core/_base_options.dart'; import 'package:graphql/src/core/observable_query.dart'; -import 'package:meta/meta.dart'; import 'package:gql/ast.dart'; import 'package:gql_exec/gql_exec.dart'; @@ -20,7 +20,9 @@ typedef OnError = void Function(OperationException error); class MutationOptions extends BaseOptions { MutationOptions({ - @required DocumentNode document, + DocumentNode document, + @Deprecated('Use `document` instead. Will be removed in 5.0.0') + DocumentNode documentNode, String operationName, Map variables = const {}, FetchPolicy fetchPolicy, @@ -30,10 +32,11 @@ class MutationOptions extends BaseOptions { this.onCompleted, this.update, this.onError, - }) : super( + }) : assert(document ?? documentNode != null, 'document must not be null'), + super( fetchPolicy: fetchPolicy, errorPolicy: errorPolicy, - document: document, + document: document ?? documentNode, operationName: operationName, variables: variables, context: context, diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 85d8fdf4a..79e8469c0 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -23,7 +23,7 @@ enum QueryLifecycle { /// Polling for results periodically polling, - /// [Observab] + /// Was polling but [ObservableQuery.stopPolling()] was called pollingStopped, /// Results are being fetched, and will trigger diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index 8d4db98bb..0ced4a3fc 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -1,3 +1,4 @@ +// ignore_for_file: deprecated_member_use_from_same_package import 'package:graphql/src/core/_base_options.dart'; import 'package:meta/meta.dart'; @@ -10,7 +11,9 @@ import 'package:graphql/src/core/policies.dart'; /// Query options. class QueryOptions extends BaseOptions { QueryOptions({ - @required DocumentNode document, + DocumentNode document, + @Deprecated('Use `document` instead. Will be removed in 5.0.0') + DocumentNode documentNode, String operationName, Map variables = const {}, FetchPolicy fetchPolicy, @@ -18,10 +21,11 @@ class QueryOptions extends BaseOptions { Object optimisticResult, this.pollInterval, Context context, - }) : super( + }) : assert(document ?? documentNode != null, 'document must not be null'), + super( fetchPolicy: fetchPolicy, errorPolicy: errorPolicy, - document: document, + document: document ?? documentNode, operationName: operationName, variables: variables, context: context, @@ -74,6 +78,8 @@ class SubscriptionOptions extends BaseOptions { class WatchQueryOptions extends QueryOptions { WatchQueryOptions({ @required DocumentNode document, + @Deprecated('Use `document` instead. Will be removed in 5.0.0') + DocumentNode documentNode, String operationName, Map variables = const {}, FetchPolicy fetchPolicy, @@ -83,9 +89,10 @@ class WatchQueryOptions extends QueryOptions { this.fetchResults = false, bool eagerlyFetchResults, Context context, - }) : eagerlyFetchResults = eagerlyFetchResults ?? fetchResults, + }) : assert(document ?? documentNode != null, 'document must not be null'), + eagerlyFetchResults = eagerlyFetchResults ?? fetchResults, super( - document: document, + document: document ?? documentNode, operationName: operationName, variables: variables, fetchPolicy: fetchPolicy, @@ -123,10 +130,14 @@ class WatchQueryOptions extends QueryOptions { /// options for fetchMore operations class FetchMoreOptions { FetchMoreOptions({ - this.document, + DocumentNode document, + @Deprecated('Use `document` instead. Will be removed in 5.0.0') + DocumentNode documentNode, this.variables = const {}, @required this.updateQuery, - }) : assert(updateQuery != null); + }) : assert(updateQuery != null), + assert(document ?? documentNode != null, 'document must not be null'), + this.document = document ?? documentNode; DocumentNode document; diff --git a/packages/graphql_flutter/example/lib/graphql_widget/main.dart b/packages/graphql_flutter/example/lib/graphql_widget/main.dart index 7a32e4e14..2857eb207 100644 --- a/packages/graphql_flutter/example/lib/graphql_widget/main.dart +++ b/packages/graphql_flutter/example/lib/graphql_widget/main.dart @@ -29,7 +29,11 @@ class GraphQLWidgetScreen extends StatelessWidget { if (ENABLE_WEBSOCKETS) { final websocketLink = WebSocketLink('ws://localhost:8080/ws/graphql'); - link = link.concat(websocketLink); + link = Link.split( + (request) => request.isSubscription, + websocketLink, + link, + ); } final client = ValueNotifier( diff --git a/v4_devnotes.md b/v4_devnotes.md deleted file mode 100644 index 39735e3b6..000000000 --- a/v4_devnotes.md +++ /dev/null @@ -1,40 +0,0 @@ -# v4 Dev Notes - -## Differences between ferry_cache and graphql cache - -- The old cache was layered, ferry_cache is stream-based -- The ferry_cache api accepts optimistic as a parameter, whereas the old cache attached optimism info to the response data - -once you serialize the query request, -if you have to deserialize the cache update, you have no access to the callback - -update cache handlers are applied twice – optimistically then from network - -handling network failure more flexibility -possibly annotations - -optimism handled by proxy - -codegen difference - -lean on hive - -building on top of ferry client and cleint generator that would make the graphql api discovery easier -`client.queryName` -creating queries at build/runtime -limitation - -- fragments as a unit of composition - -single query controller which is a stream controller, -to make a query you add an event and the response stream picks it up - -since all queries are added to the same stream controller, -pagination works by taking multiple data events and running user defined update - -you can give mutations the same fetch policies - -queries, mutations and subscriptions all run through the same controller - - -note: contribute better error messages on `operationName != operation.name` to normalize From 38cfd9b869b92b27e4790a8b4097b74ce06ed647 Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 5 Jun 2020 11:41:17 -0500 Subject: [PATCH 046/118] feat(docs): v4 changelog --- packages/graphql/CHANGELOG.md | 5 +++++ packages/graphql_flutter/CHANGELOG.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 049cdc26b..ede65c71f 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.0 (2020-06-05) + +See the [v4 changelog](../../changelog-v3-v4.md) + + # [3.1.0-beta.4](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.3...v3.1.0-beta.4) (2020-04-21) diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index 59fd2cb58..0ed7e12f1 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.1 (2020-06-05) + +See the [v4 changelog](../../changelog-v3-v4.md) + + # [3.1.0-beta.4](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.3...v3.1.0-beta.4) (2020-04-21) From ba7b6410c61a7d2cf9e28e4be2f2886a60ec4e52 Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 5 Jun 2020 23:38:52 -0500 Subject: [PATCH 047/118] fix(graphql): dumb ?? documentNode bug --- .../graphql/lib/src/core/mutation_options.dart | 5 ++++- packages/graphql/lib/src/core/query_options.dart | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/graphql/lib/src/core/mutation_options.dart b/packages/graphql/lib/src/core/mutation_options.dart index e86e87b34..9646bd2f9 100644 --- a/packages/graphql/lib/src/core/mutation_options.dart +++ b/packages/graphql/lib/src/core/mutation_options.dart @@ -32,7 +32,10 @@ class MutationOptions extends BaseOptions { this.onCompleted, this.update, this.onError, - }) : assert(document ?? documentNode != null, 'document must not be null'), + }) : assert( + (document ?? documentNode) != null, + 'document must not be null', + ), super( fetchPolicy: fetchPolicy, errorPolicy: errorPolicy, diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index 0ced4a3fc..488accb8e 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -21,7 +21,10 @@ class QueryOptions extends BaseOptions { Object optimisticResult, this.pollInterval, Context context, - }) : assert(document ?? documentNode != null, 'document must not be null'), + }) : assert( + (document ?? documentNode) != null, + 'document must not be null', + ), super( fetchPolicy: fetchPolicy, errorPolicy: errorPolicy, @@ -89,7 +92,10 @@ class WatchQueryOptions extends QueryOptions { this.fetchResults = false, bool eagerlyFetchResults, Context context, - }) : assert(document ?? documentNode != null, 'document must not be null'), + }) : assert( + (document ?? documentNode) != null, + 'document must not be null', + ), eagerlyFetchResults = eagerlyFetchResults ?? fetchResults, super( document: document ?? documentNode, @@ -136,7 +142,10 @@ class FetchMoreOptions { this.variables = const {}, @required this.updateQuery, }) : assert(updateQuery != null), - assert(document ?? documentNode != null, 'document must not be null'), + assert( + (document ?? documentNode) != null, + 'document must not be null', + ), this.document = document ?? documentNode; DocumentNode document; From ffd329446b078bae25b99294faf4403deed67c24 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 6 Jun 2020 10:14:18 -0500 Subject: [PATCH 048/118] fix(examples): ignore missing token --- packages/graphql/example/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graphql/example/lib/main.dart b/packages/graphql/example/lib/main.dart index 7ca7e07c8..974458141 100644 --- a/packages/graphql/example/lib/main.dart +++ b/packages/graphql/example/lib/main.dart @@ -29,6 +29,7 @@ GraphQLClient getGithubGraphQLClient() { final Link _link = HttpLink( 'https://api.github.com/graphql', defaultHeaders: { + // ignore: undefined_identifier 'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', }, ); From 6db46779ab5da6c7719d2df6685eac332ebff5af Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 6 Jun 2020 11:42:13 -0500 Subject: [PATCH 049/118] feat(graphql): HiveStore.open --- changelog-v3-v4.md | 36 ++++++++++--------- .../graphql/lib/src/cache/hive_store.dart | 21 ++++++++--- .../test/widgets/query_test.dart | 2 +- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/changelog-v3-v4.md b/changelog-v3-v4.md index beb15f227..b68b87b01 100644 --- a/changelog-v3-v4.md +++ b/changelog-v3-v4.md @@ -3,28 +3,30 @@ v4 aims to solve a number of sore spots, particularly with caching, largely by leveraging libraries from the https://github.com/gql-dart ecosystem. There has also been a concerted effort to add more API docstrings to the codebase. ## Cache overhaul -* There is now only a single `GraphQLCache`, which leverages [normalize](https://pub.dev/packages/normalize), + +- There is now only a single `GraphQLCache`, which leverages [normalize](https://pub.dev/packages/normalize), Giving us a much more `apollo`ish api including `typePolicies` -* `LazyCacheMap` has been deleted -* `GraphQLCache` marks itself for rebroadcasting (should fix some related issues) -* **`Store`** is now a seperate concern: +- `LazyCacheMap` has been deleted +- `GraphQLCache` marks itself for rebroadcasting (should fix some related issues) +- **`Store`** is now a seperate concern: ```dart GraphQLCache( // The default store is the InMemoryStore, which does NOT persist to disk - store: HiveStore(), + store: await HiveStore.open(), ) ``` and persistence is broken into a seperate `Store` concern. ## We now use the [gql_link system](https://github.com/gql-dart/gql/tree/master/links/gql_link) -* Most links are re-exported from `graphql/client.dart` -* `QueryOptions`, `MutationOptions`, etc are turned into -[gql_exec](https://github.com/gql-dart/gql/tree/master/links/gql_exec) `Request`s -before being sent down the link chain. -* `documentNode` is deprecated in favor of `DocumentNode document` for consistency with `gql` libraries -* We won't leave alpha until we have [full backwards compatability](https://github.com/gql-dart/gql/issues/57) + +- Most links are re-exported from `graphql/client.dart` +- `QueryOptions`, `MutationOptions`, etc are turned into + [gql_exec](https://github.com/gql-dart/gql/tree/master/links/gql_exec) `Request`s + before being sent down the link chain. +- `documentNode` is deprecated in favor of `DocumentNode document` for consistency with `gql` libraries +- We won't leave alpha until we have [full backwards compatability](https://github.com/gql-dart/gql/issues/57) ```diff final httpLink = HttpLink( @@ -88,17 +90,17 @@ Subscription( ## Minor changes -* As mentioned before, `documentNode: gql(...)` is now `document: gql(...)`. -* The exported `gql` utility adds `__typename` automatically. - **If you define your own, make sure to include `AddTypenameVisitor`, +- As mentioned before, `documentNode: gql(...)` is now `document: gql(...)`. +- The exported `gql` utility adds `__typename` automatically. + \*\*If you define your own, make sure to include `AddTypenameVisitor`, or else that your cache `dataIdFromObject` works without it ### Enums are normalized and idiomatic ```diff -- QueryResultSource.OptimisticResult +- QueryResultSource.OptimisticResult + QueryResultSource.optimisticResult -- QueryResultSource.Cache +- QueryResultSource.Cache + QueryResultSource.cache // etc @@ -108,10 +110,10 @@ Subscription( + QueryLifecycle.sideEffectsPending ``` - ### `client.fetchMore` (experimental) The `fetchMore` logic is now available for when one isn't using `watchQuery`: + ```dart /// Untested example code class MyQuery { diff --git a/packages/graphql/lib/src/cache/hive_store.dart b/packages/graphql/lib/src/cache/hive_store.dart index 948e3062a..a10afa89d 100644 --- a/packages/graphql/lib/src/cache/hive_store.dart +++ b/packages/graphql/lib/src/cache/hive_store.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:meta/meta.dart'; import 'package:hive/hive.dart'; @@ -6,14 +7,26 @@ import './store.dart'; @immutable class HiveStore extends Store { + /// Default box name for the `graphql/client.dart` cache store (`graphqlClientStore`) + static const defaultBoxName = 'graphqlClientStore'; + + /// Opens a box. Convenience pass through to [Hive.openBox]. + /// + /// If the box is already open, the instance is returned and all provided parameters are being ignored. + static final openBox = Hive.openBox; + + /// Create a [HiveStore] with a [Box] with the given [boxName] (defaults to [defaultBoxName]) + /// box from [openBox(boxName)] + static Future open([ + String boxName = defaultBoxName, + ]) async => + HiveStore(await openBox(boxName)); + @protected final Box box; /// Creates a HiveStore inititalized with [box], - /// which defaults to `Hive.box('defaultGraphqlStore')` - HiveStore([ - Box box, - ]) : box = box ?? Hive.box('defaultGraphqlStore'); + HiveStore(this.box); @override Map get(String dataId) { diff --git a/packages/graphql_flutter/test/widgets/query_test.dart b/packages/graphql_flutter/test/widgets/query_test.dart index 8126a5673..acee7734b 100644 --- a/packages/graphql_flutter/test/widgets/query_test.dart +++ b/packages/graphql_flutter/test/widgets/query_test.dart @@ -90,7 +90,7 @@ void main() { ); client = ValueNotifier( GraphQLClient( - cache: GraphQLCache(store: HiveStore()), + cache: GraphQLCache(store: await HiveStore.open()), link: httpLink, ), ); From 1118cc72a2a38fc80c1df0855ac4154e0e426b1c Mon Sep 17 00:00:00 2001 From: micimize Date: Sun, 7 Jun 2020 09:35:37 -0500 Subject: [PATCH 050/118] feat(graphql_flutter): initHiveForFlutter --- changelog-v3-v4.md | 5 ++-- .../graphql_flutter/lib/graphql_flutter.dart | 2 ++ .../graphql_flutter/lib/src/hive_init.dart | 26 +++++++++++++++++++ packages/graphql_flutter/pubspec.yaml | 4 ++- .../test/widgets/query_test.dart | 26 +++++++++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 packages/graphql_flutter/lib/src/hive_init.dart diff --git a/changelog-v3-v4.md b/changelog-v3-v4.md index b68b87b01..db5bdf357 100644 --- a/changelog-v3-v4.md +++ b/changelog-v3-v4.md @@ -11,14 +11,15 @@ v4 aims to solve a number of sore spots, particularly with caching, largely by l - **`Store`** is now a seperate concern: ```dart +/// Only necessary on flutter +await initHiveForFlutter(); + GraphQLCache( // The default store is the InMemoryStore, which does NOT persist to disk store: await HiveStore.open(), ) ``` -and persistence is broken into a seperate `Store` concern. - ## We now use the [gql_link system](https://github.com/gql-dart/gql/tree/master/links/gql_link) - Most links are re-exported from `graphql/client.dart` diff --git a/packages/graphql_flutter/lib/graphql_flutter.dart b/packages/graphql_flutter/lib/graphql_flutter.dart index 108db0943..a8f37a5c9 100644 --- a/packages/graphql_flutter/lib/graphql_flutter.dart +++ b/packages/graphql_flutter/lib/graphql_flutter.dart @@ -9,3 +9,5 @@ export 'package:graphql_flutter/src/widgets/mutation.dart'; export 'package:graphql_flutter/src/widgets/query.dart'; export 'package:graphql_flutter/src/widgets/subscription.dart'; export 'package:graphql_flutter/src/widgets/result_accumulator.dart'; + +export 'package:graphql_flutter/src/hive_init.dart'; diff --git a/packages/graphql_flutter/lib/src/hive_init.dart b/packages/graphql_flutter/lib/src/hive_init.dart new file mode 100644 index 000000000..0a4a14cd5 --- /dev/null +++ b/packages/graphql_flutter/lib/src/hive_init.dart @@ -0,0 +1,26 @@ +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart' show WidgetsFlutterBinding; + +import 'package:hive/hive.dart' show Hive; +import 'package:path_provider/path_provider.dart' + show getApplicationDocumentsDirectory; +import 'package:path/path.dart' show join; + +/// Initializes Hive with the path from [getApplicationDocumentsDirectory]. +/// +/// You can provide a [subDir] where the boxes should be stored. +/// +/// Extracted from [`hive_flutter` source][github] +/// +/// [github]: https://github.com/hivedb/hive/blob/5bf355496650017409fef4e9905e8826c5dc5bf3/hive_flutter/lib/src/hive_extensions.dart +Future initHiveForFlutter([String subDir]) async { + WidgetsFlutterBinding.ensureInitialized(); + if (!kIsWeb) { + var appDir = await getApplicationDocumentsDirectory(); + var path = appDir.path; + if (subDir != null) { + path = join(path, subDir); + } + Hive.init(path); + } +} diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 45406f600..ec71c05c3 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -15,14 +15,16 @@ dependencies: flutter: sdk: flutter meta: ^1.1.6 - path_provider: ^1.1.0 + path_provider: ^1.6.10 connectivity: ^0.4.4 + dev_dependencies: pedantic: ^1.8.0+1 mockito: ^4.0.0 flutter_test: sdk: flutter test: ^1.5.3 + environment: sdk: ">=2.6.0 <3.0.0" diff --git a/packages/graphql_flutter/test/widgets/query_test.dart b/packages/graphql_flutter/test/widgets/query_test.dart index acee7734b..10e5596ed 100644 --- a/packages/graphql_flutter/test/widgets/query_test.dart +++ b/packages/graphql_flutter/test/widgets/query_test.dart @@ -1,4 +1,7 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show MethodChannel, MethodCall; import 'package:flutter_test/flutter_test.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; @@ -14,6 +17,23 @@ final query = gql(""" } """); +/// https://flutter.dev/docs/cookbook/persistence/reading-writing-files#testing +Future mockApplicationDocumentsDirectory() async { +// Create a temporary directory. + final directory = await Directory.systemTemp.createTemp(); + + // Mock out the MethodChannel for the path_provider plugin. + const MethodChannel('plugins.flutter.io/path_provider') + .setMockMethodCallHandler((MethodCall methodCall) async { + // If you're getting the apps documents directory, return the path to the + // temp directory on the test environment instead. + if (methodCall.method == 'getApplicationDocumentsDirectory') { + return directory.path; + } + return null; + }); +} + class Page extends StatefulWidget { final Map variables; final FetchPolicy fetchPolicy; @@ -77,12 +97,18 @@ class PageState extends State { } void main() { + setUpAll(() async { + await mockApplicationDocumentsDirectory(); + }); + group('Query', () { MockHttpClient mockHttpClient; HttpLink httpLink; ValueNotifier client; setUp(() async { + await initHiveForFlutter(); + mockHttpClient = MockHttpClient(); httpLink = HttpLink( 'https://unused/graphql', From 330d89c368f953aaa721389c142f8707511f5605 Mon Sep 17 00:00:00 2001 From: micimize Date: Sun, 7 Jun 2020 09:54:40 -0500 Subject: [PATCH 051/118] packaging(graphql_flutter): hive and path dependencies --- packages/graphql/CHANGELOG.md | 2 +- packages/graphql_flutter/CHANGELOG.md | 2 +- packages/graphql_flutter/pubspec.yaml | 18 ++++++++---------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index ede65c71f..63d1ab9ae 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,4 +1,4 @@ -# 4.0.0-alpha.0 (2020-06-05) +# 4.0.0-alpha.0 (2020-06-07) See the [v4 changelog](../../changelog-v3-v4.md) diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index 0ed7e12f1..f40bb4acc 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,4 +1,4 @@ -# 4.0.0-alpha.1 (2020-06-05) +# 4.0.0-alpha.0 (2020-06-07) See the [v4 changelog](../../changelog-v3-v4.md) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index ec71c05c3..5962fdf1c 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,21 +2,18 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 3.1.0-beta.4 -authors: - - Eus Dima - - Zino Hofmann - - Michael Joseph Rosenthal +version: 4.0.0-alpha.0 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: #^3.0.1-beta.2 - path: ../graphql + graphql: ^4.0.0-alpha.0 gql_exec: ^0.2.2 flutter: sdk: flutter meta: ^1.1.6 path_provider: ^1.6.10 + path: ^1.7.0 connectivity: ^0.4.4 + hive: ^1.3.0 dev_dependencies: pedantic: ^1.8.0+1 @@ -24,10 +21,11 @@ dev_dependencies: flutter_test: sdk: flutter test: ^1.5.3 + http: ^0.12.1 environment: sdk: ">=2.6.0 <3.0.0" -dependency_overrides: - graphql: - path: ../graphql +# dependency_overrides: +# graphql: +# path: ../graphql From 645d462a73047910ba3c45a1b28483016c082131 Mon Sep 17 00:00:00 2001 From: micimize Date: Sun, 7 Jun 2020 10:45:15 -0500 Subject: [PATCH 052/118] fix(ci): loosen path version --- packages/graphql_flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 5962fdf1c..03a9b3c2d 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: sdk: flutter meta: ^1.1.6 path_provider: ^1.6.10 - path: ^1.7.0 + path: ^1.6.4 connectivity: ^0.4.4 hive: ^1.3.0 From a13eee2a14abf57f65724a49f10279d31cc3d36d Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 11 Jun 2020 03:16:02 -0400 Subject: [PATCH 053/118] test(client): add test case for rebroadcast on mutation --- .../graphql/test/graphql_client_test.dart | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index 4bb612b49..76b83d741 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -11,6 +11,26 @@ import './helpers.dart'; class MockLink extends Mock implements Link {} void main() { + const String readSingle = r''' + query ReadSingle($id: ID!) { + single(id: $id) { + id, + __typename, + name + } + } +'''; + + const String writeSingle = r''' + mutation WriteSingle($id: ID!, $name: String!) { + updateSingle(id: $id, name: $name) { + id, + __typename, + name + } + } +'''; + const String readRepositories = r''' query ReadRepositories($nRepositories: Int!) { viewer { @@ -178,6 +198,68 @@ void main() { // test('partially success query with some errors', {}); }); group('mutation', () { + test('query stream notified', () async { + when( + link.request(any), + ).thenAnswer( + (_) => Stream.fromIterable( + [ + Response( + data: { + 'single': {'id': '1', '__typename': 'Single', 'name': 'foo'}, + }, + ), + ], + ), + ); + + final ObservableQuery observable = await graphQLClientClient.watchQuery( + WatchQueryOptions(document: parseString(readSingle), eagerlyFetchResults: true, variables: {'id': '1'})); + + when( + link.request(any), + ).thenAnswer( + (_) => Stream.fromIterable( + [ + Response( + data: { + 'updateSingle': {'id': '1', '__typename': 'Single', 'name': 'bar'}, + }, + ), + ], + ), + ); + + bool result = false; + + observable.stream.listen((event) { + if (event.data != null && event.data['single']['name'] == 'bar') { + result = true; + } + }); + + final hit = Future.doWhile(() async { + if (result) { + return false; + } + + await Future.delayed(Duration(milliseconds: 10)); + + return true; + }); + + final variables = {'id': '1', 'name': 'bar'}; + + final QueryResult response = + await graphQLClientClient.mutate(MutationOptions(document: parseString(writeSingle), variables: variables)); + + expect(response.data['updateSingle']['name'], variables['name']); + + await Future.any([hit, Future.delayed(const Duration(seconds: 3))]); + + expect(result, true); + }); + test('successful mutation', () async { final MutationOptions _options = MutationOptions( document: parseString(addStar), From 75393c2763c8b232aea7a719fa54d53a5885f995 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 11 Jun 2020 09:24:32 -0400 Subject: [PATCH 054/118] fix(client): mutation not firing observer callbacks --- packages/graphql/lib/src/core/query_manager.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index f0f16375d..b0a73cd65 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -112,6 +112,8 @@ class QueryManager { await callback(result); } + maybeRebroadcastQueries(); + return result; }); } From 7b08fca4ccc29a01fcaed55f591fa9b32a8aca8e Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 17 Jun 2020 12:01:07 -0500 Subject: [PATCH 055/118] refactor(client): use idiomatic emitsInOrder for mutation "query stream notified" test --- .../graphql/test/graphql_client_test.dart | 92 +++++++++++-------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index 76b83d741..eb08a8d92 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -199,65 +199,79 @@ void main() { }); group('mutation', () { test('query stream notified', () async { + final initialQueryResponse = Response( + data: { + 'single': { + 'id': '1', + '__typename': 'Single', + 'name': 'initialQueryName', + }, + }, + ); when( link.request(any), ).thenAnswer( (_) => Stream.fromIterable( + [initialQueryResponse], + ), + ); + + final ObservableQuery observable = await graphQLClientClient.watchQuery( + WatchQueryOptions( + document: parseString(readSingle), + eagerlyFetchResults: true, + variables: {'id': '1'}, + ), + ); + + expect( + observable.stream, + emitsInOrder( [ - Response( - data: { - 'single': {'id': '1', '__typename': 'Single', 'name': 'foo'}, - }, + // we have no optimistic result + isA().having( + (result) => result.isLoading, + 'loading result', + true, ), + isA().having( + (result) => result.data['single']['name'], + 'initial query result', + 'initialQueryName', + ), + isA().having( + (result) => result.data['single']['name'], + 'result caused by mutation', + 'newNameFromMutation', + ) ], ), ); - final ObservableQuery observable = await graphQLClientClient.watchQuery( - WatchQueryOptions(document: parseString(readSingle), eagerlyFetchResults: true, variables: {'id': '1'})); - + final mutationResponseWithNewName = Response( + data: { + 'updateSingle': { + 'id': '1', + '__typename': 'Single', + 'name': 'newNameFromMutation', + }, + }, + ); when( link.request(any), ).thenAnswer( (_) => Stream.fromIterable( - [ - Response( - data: { - 'updateSingle': {'id': '1', '__typename': 'Single', 'name': 'bar'}, - }, - ), - ], + [mutationResponseWithNewName], ), ); - bool result = false; - - observable.stream.listen((event) { - if (event.data != null && event.data['single']['name'] == 'bar') { - result = true; - } - }); - - final hit = Future.doWhile(() async { - if (result) { - return false; - } - - await Future.delayed(Duration(milliseconds: 10)); + final variables = {'id': '1', 'name': 'newNameFromMutation'}; - return true; - }); - - final variables = {'id': '1', 'name': 'bar'}; - - final QueryResult response = - await graphQLClientClient.mutate(MutationOptions(document: parseString(writeSingle), variables: variables)); + final QueryResult response = await graphQLClientClient.mutate( + MutationOptions( + document: parseString(writeSingle), variables: variables)); expect(response.data['updateSingle']['name'], variables['name']); - - await Future.any([hit, Future.delayed(const Duration(seconds: 3))]); - - expect(result, true); }); test('successful mutation', () async { From aba3ccc84138cf2810a4868d7df6166b363c22b6 Mon Sep 17 00:00:00 2001 From: Aravind Vemula Date: Wed, 24 Jun 2020 19:00:16 +0530 Subject: [PATCH 056/118] Fixed issue#676 type '_InternalLinkedHashMap' is not a subtype of type 'Map' in type cast --- packages/graphql/lib/src/utilities/helpers.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index bbfaa1791..a859443d8 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -13,7 +13,7 @@ Map _recursivelyAddAll( target = Map.from(target); source.forEach((String key, dynamic value) { if (target.containsKey(key) && - target[key] is Map && + target[key] is Map && value != null && value is Map) { target[key] = _recursivelyAddAll( From 65fdcb2600257f8982496e5191424f42365f7f39 Mon Sep 17 00:00:00 2001 From: Aravind Vemula Date: Wed, 24 Jun 2020 19:09:23 +0530 Subject: [PATCH 057/118] Fix issue #676 type '_InternalLinkedHashMap' is not a subtype of type 'Map' in type cast --- packages/graphql/lib/src/utilities/helpers.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index a859443d8..4c1d0b9af 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -10,7 +10,7 @@ Map _recursivelyAddAll( Map target, Map source, ) { - target = Map.from(target); + target = Map.from(target); source.forEach((String key, dynamic value) { if (target.containsKey(key) && target[key] is Map && From 376fb13dda9af310a1ee7d67f4f48f328a0fd6be Mon Sep 17 00:00:00 2001 From: Aravind Vemula Date: Wed, 24 Jun 2020 19:22:02 +0530 Subject: [PATCH 058/118] Update pubspec.yaml --- packages/graphql_flutter/pubspec.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 03a9b3c2d..0a630fec2 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -5,7 +5,10 @@ description: version: 4.0.0-alpha.0 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.0 + graphql: + url: https://github.com/brewhackers-technologies/graphql-flutter.git + ref: modularization + path: packages/graphql gql_exec: ^0.2.2 flutter: sdk: flutter From 5445066bd0831f76677f4e0a47656873191050f7 Mon Sep 17 00:00:00 2001 From: Aravind Vemula Date: Wed, 24 Jun 2020 19:23:26 +0530 Subject: [PATCH 059/118] Update pubspec.yaml --- packages/graphql_flutter/pubspec.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 0a630fec2..a5d2a52e1 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -5,10 +5,11 @@ description: version: 4.0.0-alpha.0 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: - url: https://github.com/brewhackers-technologies/graphql-flutter.git - ref: modularization - path: packages/graphql + graphql: + git: + url: https://github.com/brewhackers-technologies/graphql-flutter.git + ref: modularization + path: packages/graphql gql_exec: ^0.2.2 flutter: sdk: flutter From 1c287efac388197b91036eda686f3fad5eb67237 Mon Sep 17 00:00:00 2001 From: Aravind Vemula Date: Wed, 24 Jun 2020 19:24:26 +0530 Subject: [PATCH 060/118] Update pubspec.yaml --- packages/graphql_flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index a5d2a52e1..0a8f36247 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.0 +version: latest homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: graphql: From 87d6be77ef0f0f3f3296431dd4e6752824f88408 Mon Sep 17 00:00:00 2001 From: Aravind Vemula Date: Wed, 24 Jun 2020 19:25:03 +0530 Subject: [PATCH 061/118] Update pubspec.yaml --- packages/graphql_flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 0a8f36247..fa6c4b587 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: latest +version: 4.0.0.alpha.0 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: graphql: From 07b7409531695757ed05f5fed20894fd842ec8b5 Mon Sep 17 00:00:00 2001 From: Aravind Vemula Date: Wed, 24 Jun 2020 19:25:23 +0530 Subject: [PATCH 062/118] Update pubspec.yaml --- packages/graphql_flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index fa6c4b587..a5d2a52e1 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0.alpha.0 +version: 4.0.0-alpha.0 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: graphql: From 681f29b215dde3dbc40e9d16975cabc311748915 Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 17 Jun 2020 12:23:48 -0500 Subject: [PATCH 063/118] packaging: release v4 alpha.1 --- packages/graphql/CHANGELOG.md | 4 ++++ packages/graphql/pubspec.yaml | 2 +- packages/graphql_flutter/CHANGELOG.md | 4 ++++ packages/graphql_flutter/pubspec.yaml | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 63d1ab9ae..73aa864dc 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,7 @@ +# 4.0.0-alpha.1 (2020-06-17) +* **client:** `maybeRebroadcast` on `mutation` ([75393c2](https://github.com/zino-app/graphql-flutter/commit/75393c2763c8b232aea7a719fa54d53a5885f995)) + + # 4.0.0-alpha.0 (2020-06-07) See the [v4 changelog](../../changelog-v3-v4.md) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 63b1ac973..a50634232 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.0 +version: 4.0.0-alpha.1 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index f40bb4acc..afd546bc9 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +# 4.0.0-alpha.1 (2020-06-17) +* **client:** `maybeRebroadcast` on `mutation` ([75393c2](https://github.com/zino-app/graphql-flutter/commit/75393c2763c8b232aea7a719fa54d53a5885f995)) + + # 4.0.0-alpha.0 (2020-06-07) See the [v4 changelog](../../changelog-v3-v4.md) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index a5d2a52e1..1a6288349 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.0 +version: 4.0.0-alpha.1 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: graphql: From 2a3e6a11edfe85d322c07514d238d89093e451a0 Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 19 Jun 2020 09:23:26 -0500 Subject: [PATCH 064/118] feat(tests): test subscriptions --- .../graphql/test/graphql_client_test.dart | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index eb08a8d92..d47328f85 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -319,5 +319,65 @@ void main() { expect(viewerHasStarred, true); }); }); + + group('subscription', () { + test('results', () async { + final responses = [ + { + 'id': '1', + 'name': 'first', + }, + { + 'id': '2', + 'name': 'second', + }, + ].map((item) => Response( + data: { + 'item': { + '__typename': 'Item', + ...item, + }, + }, + )); + when( + link.request(any), + ).thenAnswer( + (_) => Stream.fromIterable(responses), + ); + + final stream = graphQLClientClient.subscribe( + SubscriptionOptions( + document: parseString( + r''' + subscription { + item { + id + name + } + } + ''', + ), + ), + ); + + expect( + stream, + emitsInOrder( + [ + isA().having( + (result) => result.data['item']['name'], + 'first subscription item', + 'first', + ), + isA().having( + (result) => result.data['item']['name'], + 'second subscription item', + 'second', + ) + ], + ), + ); + }); + }); }); } From dd3e1b8010dd73757030d83ee629ee8f867c0dbd Mon Sep 17 00:00:00 2001 From: micimize Date: Wed, 24 Jun 2020 12:28:33 -0500 Subject: [PATCH 065/118] working on making hive store more accessible --- changelog-v3-v4.md | 5 ++++- examples/starwars/lib/client_provider.dart | 13 +++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/changelog-v3-v4.md b/changelog-v3-v4.md index db5bdf357..a2e49a67a 100644 --- a/changelog-v3-v4.md +++ b/changelog-v3-v4.md @@ -12,7 +12,10 @@ v4 aims to solve a number of sore spots, particularly with caching, largely by l ```dart /// Only necessary on flutter -await initHiveForFlutter(); +void main() async { + await initHiveForFlutter(); + runApp(MyApp()); +} GraphQLCache( // The default store is the InMemoryStore, which does NOT persist to disk diff --git a/examples/starwars/lib/client_provider.dart b/examples/starwars/lib/client_provider.dart index 977f6a62b..2a6f27b85 100644 --- a/examples/starwars/lib/client_provider.dart +++ b/examples/starwars/lib/client_provider.dart @@ -12,12 +12,17 @@ String uuidFromObject(Object object) { return null; } -final GraphQLCache cache = GraphQLCache(); + GraphQLCache _cache; -ValueNotifier clientFor({ +Future get cache async { +_cache ??= GraphQLCache(store: await HiveStore.open()); +return _cache; +} + +Future> clientFor({ @required String uri, String subscriptionUri, -}) { +}) async { Link link = HttpLink(uri); if (subscriptionUri != null) { final WebSocketLink websocketLink = WebSocketLink( @@ -29,7 +34,7 @@ ValueNotifier clientFor({ return ValueNotifier( GraphQLClient( - cache: cache, + cache: await cache, link: link, ), ); From 292b2a0755b71bc6dbc5aa3173e2c4ab5e139f03 Mon Sep 17 00:00:00 2001 From: micimize Date: Tue, 21 Jul 2020 15:57:10 -0700 Subject: [PATCH 066/118] better hive api --- packages/graphql_flutter/lib/src/hive_init.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/graphql_flutter/lib/src/hive_init.dart b/packages/graphql_flutter/lib/src/hive_init.dart index 0a4a14cd5..ac13ace91 100644 --- a/packages/graphql_flutter/lib/src/hive_init.dart +++ b/packages/graphql_flutter/lib/src/hive_init.dart @@ -6,6 +6,8 @@ import 'package:path_provider/path_provider.dart' show getApplicationDocumentsDirectory; import 'package:path/path.dart' show join; +import 'package:graphql/client.dart' show HiveStore; + /// Initializes Hive with the path from [getApplicationDocumentsDirectory]. /// /// You can provide a [subDir] where the boxes should be stored. @@ -13,7 +15,7 @@ import 'package:path/path.dart' show join; /// Extracted from [`hive_flutter` source][github] /// /// [github]: https://github.com/hivedb/hive/blob/5bf355496650017409fef4e9905e8826c5dc5bf3/hive_flutter/lib/src/hive_extensions.dart -Future initHiveForFlutter([String subDir]) async { +Future initHiveForFlutter({String subDir, Iterable boxes = const [ HiveStore.defaultBoxName ] }) async { WidgetsFlutterBinding.ensureInitialized(); if (!kIsWeb) { var appDir = await getApplicationDocumentsDirectory(); @@ -23,4 +25,9 @@ Future initHiveForFlutter([String subDir]) async { } Hive.init(path); } + + for (var box in boxes){ + await Hive.openBox(box); + } + } From 22db4f7ffac1ca7abd00a75a060117ec5b9e4375 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 23 Jul 2020 11:22:26 -0700 Subject: [PATCH 067/118] fix(examples): starwars example cache --- examples/starwars/lib/client_provider.dart | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/examples/starwars/lib/client_provider.dart b/examples/starwars/lib/client_provider.dart index 2a6f27b85..977f6a62b 100644 --- a/examples/starwars/lib/client_provider.dart +++ b/examples/starwars/lib/client_provider.dart @@ -12,17 +12,12 @@ String uuidFromObject(Object object) { return null; } - GraphQLCache _cache; +final GraphQLCache cache = GraphQLCache(); -Future get cache async { -_cache ??= GraphQLCache(store: await HiveStore.open()); -return _cache; -} - -Future> clientFor({ +ValueNotifier clientFor({ @required String uri, String subscriptionUri, -}) async { +}) { Link link = HttpLink(uri); if (subscriptionUri != null) { final WebSocketLink websocketLink = WebSocketLink( @@ -34,7 +29,7 @@ Future> clientFor({ return ValueNotifier( GraphQLClient( - cache: await cache, + cache: cache, link: link, ), ); From 2d1a7f2e367f57f6ff2f968814045fb5edf15085 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 23 Jul 2020 11:23:02 -0700 Subject: [PATCH 068/118] feat(graphql): HiveStore api improvements, fetchmore fixes --- packages/graphql/lib/src/cache/hive_store.dart | 9 +++++++-- packages/graphql/lib/src/core/query_options.dart | 4 ---- packages/graphql_flutter/pubspec.yaml | 6 +----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/graphql/lib/src/cache/hive_store.dart b/packages/graphql/lib/src/cache/hive_store.dart index a10afa89d..a9efb53ed 100644 --- a/packages/graphql/lib/src/cache/hive_store.dart +++ b/packages/graphql/lib/src/cache/hive_store.dart @@ -25,8 +25,13 @@ class HiveStore extends Store { @protected final Box box; - /// Creates a HiveStore inititalized with [box], - HiveStore(this.box); + /// Creates a HiveStore inititalized with the given [box], defaulting to `Hive.box(defaultBoxName)` + /// + /// **N.B.**: [box] must already be [opened] with either [openBox], [open], or `initHiveForFlutter` from `graphql_flutter`. + /// This lets us decouple the async initialization logic, making store usage elsewhere much more straightforward. + /// + /// [opened]: https://docs.hivedb.dev/#/README?id=open-a-box + HiveStore([ Box box ]): this.box = box ?? Hive.box(defaultBoxName); @override Map get(String dataId) { diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index 488accb8e..7297ca9d8 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -142,10 +142,6 @@ class FetchMoreOptions { this.variables = const {}, @required this.updateQuery, }) : assert(updateQuery != null), - assert( - (document ?? documentNode) != null, - 'document must not be null', - ), this.document = document ?? documentNode; DocumentNode document; diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 1a6288349..626480225 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -6,10 +6,7 @@ version: 4.0.0-alpha.1 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: graphql: - git: - url: https://github.com/brewhackers-technologies/graphql-flutter.git - ref: modularization - path: packages/graphql + path: ../graphql gql_exec: ^0.2.2 flutter: sdk: flutter @@ -29,7 +26,6 @@ dev_dependencies: environment: sdk: ">=2.6.0 <3.0.0" - # dependency_overrides: # graphql: # path: ../graphql From 2f874ecde038e16332bb51243afb167ac0421e35 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 23 Jul 2020 11:23:25 -0700 Subject: [PATCH 069/118] feat(examples): starwars hivestore usage --- examples/starwars/lib/client_provider.dart | 4 +--- examples/starwars/lib/main.dart | 6 +++++- examples/starwars/pubspec.yaml | 6 ++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/starwars/lib/client_provider.dart b/examples/starwars/lib/client_provider.dart index 977f6a62b..3cf0266b1 100644 --- a/examples/starwars/lib/client_provider.dart +++ b/examples/starwars/lib/client_provider.dart @@ -12,8 +12,6 @@ String uuidFromObject(Object object) { return null; } -final GraphQLCache cache = GraphQLCache(); - ValueNotifier clientFor({ @required String uri, String subscriptionUri, @@ -29,7 +27,7 @@ ValueNotifier clientFor({ return ValueNotifier( GraphQLClient( - cache: cache, + cache: GraphQLCache(store: HiveStore()), link: link, ), ); diff --git a/examples/starwars/lib/main.dart b/examples/starwars/lib/main.dart index c04668935..cf87a5bc7 100644 --- a/examples/starwars/lib/main.dart +++ b/examples/starwars/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:universal_platform/universal_platform.dart'; import './client_provider.dart'; @@ -18,7 +19,10 @@ String get host { final graphqlEndpoint = 'http://$host:3000/graphql'; final subscriptionEndpoint = 'ws://$host:3000/subscriptions'; -void main() => runApp(MyApp()); +void main() async { + await initHiveForFlutter(); + runApp(MyApp()); +} class MyApp extends StatelessWidget { @override diff --git a/examples/starwars/pubspec.yaml b/examples/starwars/pubspec.yaml index 163930343..e58346095 100644 --- a/examples/starwars/pubspec.yaml +++ b/examples/starwars/pubspec.yaml @@ -8,15 +8,14 @@ dependencies: path: ../../packages/graphql_flutter graphql: path: ../../packages/graphql - universal_platform: ^0.1.3 + universal_platform: + ^0.1.3 # https://github.com/flutter/flutter/issues/36126#issuecomment-596215587 graphql_starwars_test_server: ^0.1.0 flutter: uses-material-design: true - - dependency_overrides: graphql_server: git: @@ -27,4 +26,3 @@ dependency_overrides: git: url: git@github.com:micimize/angel.git path: packages/graphql/graphql_parser - From 1f2192710531a920be6c7df1b22da105c8cdc11c Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 24 Jul 2020 10:14:16 -0700 Subject: [PATCH 070/118] docs(v4): direct cache access --- changelog-v3-v4.md | 9 ++++++++- packages/graphql/pubspec.yaml | 6 ++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/changelog-v3-v4.md b/changelog-v3-v4.md index a2e49a67a..a7dc65db6 100644 --- a/changelog-v3-v4.md +++ b/changelog-v3-v4.md @@ -5,7 +5,10 @@ v4 aims to solve a number of sore spots, particularly with caching, largely by l ## Cache overhaul - There is now only a single `GraphQLCache`, which leverages [normalize](https://pub.dev/packages/normalize), - Giving us a much more `apollo`ish api including `typePolicies` + Giving us a much more `apollo`ish API. + - [`typePolicies`] + - [direct cache access] via `readQuery`, `writeQuery`, `readFragment`, and `writeFragment` + All of which can which can be used for [local state management] - `LazyCacheMap` has been deleted - `GraphQLCache` marks itself for rebroadcasting (should fix some related issues) - **`Store`** is now a seperate concern: @@ -140,3 +143,7 @@ class MyQuery { } } ``` + +[local state management]: https://www.apollographql.com/docs/tutorial/local-state/#update-local-data +[`typePolicies`]: https://www.apollographql.com/docs/react/caching/cache-configuration/#the-typepolicy-type +[direct cache access]: https://www.apollographql.com/docs/react/caching/cache-interaction/ diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index a50634232..5265f6483 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -11,10 +11,10 @@ dependencies: gql: ^0.12.0 gql_exec: ^0.2.2 gql_link: ^0.3.0 - gql_http_link: ^0.2.9 + gql_http_link: 0.2.11-alpha+1594319799546 gql_websocket_link: ^0.1.1-alpha gql_transform_link: ^0.1.5 - gql_error_link: ^0.1.1-alpha + gql_error_link: ^0.1.1-alpha gql_dedupe_link: ^1.0.10 hive: ^1.3.0 @@ -22,7 +22,6 @@ dependencies: http: ^0.12.1 collection: ^1.14.12 - dev_dependencies: pedantic: ^1.8.0+1 mockito: ^4.0.0 @@ -31,4 +30,3 @@ dev_dependencies: environment: sdk: ">=2.6.0 <3.0.0" - From 0b3fbd9a4d3d0f9bded2bd9a9fdf26e3bfe983df Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 24 Jul 2020 15:58:50 -0700 Subject: [PATCH 071/118] fix(graphql): simplified AuthLink --- changelog-v3-v4.md | 2 +- packages/graphql/lib/src/links/auth_link.dart | 93 ++++++++++--------- packages/graphql/pubspec.yaml | 2 +- 3 files changed, 49 insertions(+), 48 deletions(-) diff --git a/changelog-v3-v4.md b/changelog-v3-v4.md index a7dc65db6..611db87c5 100644 --- a/changelog-v3-v4.md +++ b/changelog-v3-v4.md @@ -22,7 +22,7 @@ void main() async { GraphQLCache( // The default store is the InMemoryStore, which does NOT persist to disk - store: await HiveStore.open(), + store: GraphQLCache(store: HiveStore()), ) ``` diff --git a/packages/graphql/lib/src/links/auth_link.dart b/packages/graphql/lib/src/links/auth_link.dart index 272db12b5..09be10a60 100644 --- a/packages/graphql/lib/src/links/auth_link.dart +++ b/packages/graphql/lib/src/links/auth_link.dart @@ -1,69 +1,70 @@ import 'dart:async'; +import 'package:graphql/client.dart'; import 'package:meta/meta.dart'; import "package:gql_exec/gql_exec.dart"; import "package:gql_http_link/gql_http_link.dart"; import "package:gql_link/gql_link.dart"; -import "package:gql_error_link/gql_error_link.dart"; import "package:gql_transform_link/gql_transform_link.dart"; -// Mostly taken from -// https://github.com/gql-dart/gql/blob/master/examples/gql_example_http_auth_link/lib/http_auth_link.dart -class AuthLink extends Link { - Link _link; - String _token; +typedef _RequestTransformer = FutureOr Function(Request request); - final FutureOr Function() getToken; - - final String headerKey; +typedef OnException = FutureOr Function( + HttpLinkServerException exception, +); +/// Simple header-based authentication link that adds [headerKey]: [getToken()] to every request. +/// +/// If a lazy or exception-based authentication link is needed for your use case, +/// implementing your own from the [gql reference auth link] or opening an issue. +/// +/// [gql reference auth link]: https://github.com/gql-dart/gql/blob/1884596904a411363165bcf3c7cfa9dcc2a61c26/examples/gql_example_http_auth_link/lib/http_auth_link.dart +class AuthLink extends _AsyncReqTransformLink { AuthLink({ @required this.getToken, this.headerKey = 'Authorization', - }) { - _link = Link.concat( - ErrorLink(onException: handleException), - TransformLink(requestTransformer: transformRequest), - ); - } - - Future updateToken() async { - _token = await getToken(); - } + }) : super(requestTransformer: transform(headerKey, getToken)); - Stream handleException( - Request request, - NextLink forward, - LinkException exception, - ) async* { - if (exception is HttpLinkServerException && - exception.response.statusCode == 401) { - await updateToken(); + /// Authentication callback. Note – must include prefixes, e.g. `'Bearer $token'` + final FutureOr Function() getToken; - yield* forward(request); + /// Header key to set to the result of [getToken] + final String headerKey; - return; - } + static _RequestTransformer transform( + String headerKey, + FutureOr Function() getToken, + ) => + (Request request) async { + final token = await getToken(); + return request.updateContextEntry( + (headers) => HttpLinkHeaders( + headers: { + ...headers?.headers ?? {}, + headerKey: token, + }, + ), + ); + }; +} - throw exception; - } +/// Version of [TransformLink] that handles async transforms +class _AsyncReqTransformLink extends Link { + final _RequestTransformer requestTransformer; - Request transformRequest(Request request) => - request.updateContextEntry( - (headers) => HttpLinkHeaders( - headers: { - ...headers?.headers ?? {}, - headerKey: _token, - }, - ), - ); + _AsyncReqTransformLink({ + this.requestTransformer, + }) : assert(requestTransformer != null); @override - Stream request(Request request, [forward]) async* { - if (_token == null) { - await updateToken(); - } + Stream request( + Request request, [ + NextLink forward, + ]) async* { + final req = requestTransformer != null + ? await requestTransformer(request) + : request; - yield* _link.request(request, forward); + yield* forward(req); } } diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 5265f6483..d0d695ed5 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: gql: ^0.12.0 gql_exec: ^0.2.2 gql_link: ^0.3.0 - gql_http_link: 0.2.11-alpha+1594319799546 + gql_http_link: ^0.2.11-alpha+1594319799546 gql_websocket_link: ^0.1.1-alpha gql_transform_link: ^0.1.5 gql_error_link: ^0.1.1-alpha From 0dbbe7866353ae6fa956a90ab442953e8a0154f1 Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 24 Jul 2020 18:49:08 -0700 Subject: [PATCH 072/118] test deeplyMergeLeft --- packages/graphql/test/test_helpers.dart | 70 +++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/graphql/test/test_helpers.dart diff --git a/packages/graphql/test/test_helpers.dart b/packages/graphql/test/test_helpers.dart new file mode 100644 index 000000000..adf5b9346 --- /dev/null +++ b/packages/graphql/test/test_helpers.dart @@ -0,0 +1,70 @@ +import 'dart:collection'; + +import 'package:test/test.dart'; + +import 'package:graphql/src/utilities/helpers.dart'; + +void main() { + group('deeplyMergeLeft', () { + test('shallow', () { + expect( + deeplyMergeLeft([ + {'keyA': 'a1'}, + {'keyA': 'a2', 'keyB': 'b2'}, + {'keyB': 'b3'} + ]), + equals({'keyA': 'a2', 'keyB': 'b3'}), + ); + }); + + test('deep', () { + expect( + deeplyMergeLeft([ + { + 'keyA': 'a1', + 'keyB': { + 'keyC': {'keyD': 'd1'} + } + }, + { + 'keyA': 'a2', + 'keyB': { + 'keyC': {'keyD': 'd2'} + } + }, + ]), + equals({ + 'keyA': 'a2', + 'keyB': { + 'keyC': {'keyD': 'd2'} + } + }), + ); + }); + + test('deep hashmaps are merged', () { + expect( + deeplyMergeLeft([ + HashMap.from({ + 'keyA': 'a1', + 'keyB': { + 'keyC': HashMap.from({'keyD': 'd1'}) + } + }), + { + 'keyA': 'a2', + 'keyB': { + 'keyC': HashMap.from({'keyD': 'd2'}) + } + }, + ]), + equals({ + 'keyA': 'a2', + 'keyB': { + 'keyC': {'keyD': 'd2'} + } + }), + ); + }); + }); +} From 01768c5acae9acb7cb840051280471447a2ffe2f Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 24 Jul 2020 19:02:38 -0700 Subject: [PATCH 073/118] packaging(graphql): alpha.2 --- packages/graphql/CHANGELOG.md | 9 +++++++++ packages/graphql/pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 73aa864dc..5f7327a89 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,12 @@ +# 4.0.0-alpha.2 (2020-07-24) +* **client**: simplified AuthLink ([0b3fbd9](https://github.com/zino-app/graphql-flutter/commit/0b3fbd9a4d3d0f9bded2bd9a9fdf26e3bfe983df)) +* **docs**: direct cache access in changelog ([1f21927](https://github.com/zino-app/graphql-flutter/commit/1f2192710531a920be6c7df1b22da105c8cdc11c)) +* **examples**: starwars hivestore usage ([2f874ec](https://github.com/zino-app/graphql-flutter/commit/2f874ecde038e16332bb51243afb167ac0421e35)) +* **graphql**: `HiveStore` api improvements ([2d1a7f2](https://github.com/zino-app/graphql-flutter/commit/2d1a7f2e367f57f6ff2f968814045fb5edf15085)) +* **fix**: `FetchMoreOptions` was throwing without `document` ([2d1a7f2](https://github.com/zino-app/graphql-flutter/commit/2d1a7f2e367f57f6ff2f968814045fb5edf15085)) +* **fix**: `deeplyMergeLeft` type error ([65fdcb2](https://github.com/zino-app/graphql-flutter/commit/65fdcb2600257f8982496e5191424f42365f7f39)) + + # 4.0.0-alpha.1 (2020-06-17) * **client:** `maybeRebroadcast` on `mutation` ([75393c2](https://github.com/zino-app/graphql-flutter/commit/75393c2763c8b232aea7a719fa54d53a5885f995)) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index d0d695ed5..6f10abda8 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.1 +version: 4.0.0-alpha.2 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 From 76c8bf1c8257cafae38c8e5e03aa8ed2385b02ce Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 24 Jul 2020 19:03:27 -0700 Subject: [PATCH 074/118] packaging(graphql_flutter): alpha.2 --- packages/graphql_flutter/CHANGELOG.md | 9 +++++++++ packages/graphql_flutter/pubspec.yaml | 7 ++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index afd546bc9..b450b8261 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,12 @@ +# 4.0.0-alpha.2 (2020-06-17) +* **client**: simplified AuthLink ([0b3fbd9](https://github.com/zino-app/graphql-flutter/commit/0b3fbd9a4d3d0f9bded2bd9a9fdf26e3bfe983df)) +* **docs**: direct cache access in changelog ([1f21927](https://github.com/zino-app/graphql-flutter/commit/1f2192710531a920be6c7df1b22da105c8cdc11c)) +* **examples**: starwars hivestore usage ([2f874ec](https://github.com/zino-app/graphql-flutter/commit/2f874ecde038e16332bb51243afb167ac0421e35)) +* **graphql**: `HiveStore` api improvements ([2d1a7f2](https://github.com/zino-app/graphql-flutter/commit/2d1a7f2e367f57f6ff2f968814045fb5edf15085)) +* **fix**: `FetchMoreOptions` was throwing without `document` ([2d1a7f2](https://github.com/zino-app/graphql-flutter/commit/2d1a7f2e367f57f6ff2f968814045fb5edf15085)) +* **fix**: `deeplyMergeLeft` type error ([65fdcb2](https://github.com/zino-app/graphql-flutter/commit/65fdcb2600257f8982496e5191424f42365f7f39)) + + # 4.0.0-alpha.1 (2020-06-17) * **client:** `maybeRebroadcast` on `mutation` ([75393c2](https://github.com/zino-app/graphql-flutter/commit/75393c2763c8b232aea7a719fa54d53a5885f995)) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 626480225..7526c1de2 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,11 +2,11 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.1 +version: 4.0.0-alpha.2 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: - path: ../graphql + graphql: ^4.0.0-alpha.2 + #path: ../graphql gql_exec: ^0.2.2 flutter: sdk: flutter @@ -26,6 +26,7 @@ dev_dependencies: environment: sdk: ">=2.6.0 <3.0.0" + # dependency_overrides: # graphql: # path: ../graphql From 2ba6c743a7317c3df05c3f1c5e8e3e3cd44d6827 Mon Sep 17 00:00:00 2001 From: micimize Date: Mon, 27 Jul 2020 11:37:59 -0500 Subject: [PATCH 075/118] fix(graphql): don't close mutations after callbacks --- packages/graphql/lib/src/core/observable_query.dart | 2 +- packages/graphql_flutter/lib/src/widgets/mutation.dart | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 79e8469c0..632710a86 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -289,9 +289,9 @@ class ObservableQuery { lifecycle = QueryLifecycle.completed; close(); } + // the mutation has been completed, but disposal has not been requested if (lifecycle == QueryLifecycle.sideEffectsPending) { lifecycle = QueryLifecycle.completed; - close(); } } } diff --git a/packages/graphql_flutter/lib/src/widgets/mutation.dart b/packages/graphql_flutter/lib/src/widgets/mutation.dart index cf9572f70..88fda9b49 100644 --- a/packages/graphql_flutter/lib/src/widgets/mutation.dart +++ b/packages/graphql_flutter/lib/src/widgets/mutation.dart @@ -102,8 +102,7 @@ class MutationState extends State { return (observableQuery ..variables = variables ..options.optimisticResult = optimisticResult - ..onData(mutationCallbacks - .callbacks) // add callbacks to observable // interesting + ..onData(mutationCallbacks.callbacks) // add callbacks to observable ) .fetchResults(); } From 5b6e3d06a7dc56888dcbfc4c395ea51985c10f1c Mon Sep 17 00:00:00 2001 From: micimize Date: Mon, 27 Jul 2020 11:40:36 -0500 Subject: [PATCH 076/118] fix(examples): update ios files for graphql_flutter/example --- packages/graphql_flutter/example/ios/Podfile | 69 ++++++------------- .../ios/Runner.xcodeproj/project.pbxproj | 13 ++-- .../xcshareddata/xcschemes/Runner.xcscheme | 12 +--- .../xcshareddata/IDEWorkspaceChecks.plist | 8 +++ 4 files changed, 42 insertions(+), 60 deletions(-) create mode 100644 packages/graphql_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/graphql_flutter/example/ios/Podfile b/packages/graphql_flutter/example/ios/Podfile index 7c6cb6f63..f7d6a5e68 100644 --- a/packages/graphql_flutter/example/ios/Podfile +++ b/packages/graphql_flutter/example/ios/Podfile @@ -4,60 +4,35 @@ # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end - pods_ary = [] - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) { |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - pods_ary.push({:name => podname, :path => podpath}); - else - puts "Invalid plugin specification: #{line}" - end - } - return pods_ary + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + target 'Runner' do - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - - # Flutter Pods - generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') - if generated_xcode_build_settings.empty? - puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." - end - generated_xcode_build_settings.map { |p| - if p[:name] == 'FLUTTER_FRAMEWORK_DIR' - symlink = File.join('.symlinks', 'flutter') - File.symlink(File.dirname(p[:path]), symlink) - pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) - end - } - - # Plugin Pods - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.map { |p| - symlink = File.join('.symlinks', 'plugins', p[:name]) - File.symlink(p[:path], symlink) - pod p[:name], :path => File.join(symlink, 'ios') - } + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end + flutter_additional_ios_build_settings(target) end end diff --git a/packages/graphql_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/graphql_flutter/example/ios/Runner.xcodeproj/project.pbxproj index ad353e3d3..dfc20fdcf 100644 --- a/packages/graphql_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/graphql_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 85BD965404134B0319457AD8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A9CD12FDAA2BE6D4AEA7ACE /* libPods-Runner.a */; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; @@ -166,7 +165,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0910; + LastUpgradeCheck = 1130; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -179,6 +178,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -200,7 +200,6 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -262,7 +261,7 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", + "${PODS_ROOT}/../Flutter/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( @@ -312,6 +311,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -321,12 +321,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -365,6 +367,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -374,12 +377,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; diff --git a/packages/graphql_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/graphql_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1263ac84b..eff2bca03 100644 --- a/packages/graphql_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/graphql_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - + + + + IDEDidComputeMac32BitWarning + + + From 3d67b385705bfb0059e671e67d5237e04a863554 Mon Sep 17 00:00:00 2001 From: micimize Date: Mon, 27 Jul 2020 11:44:04 -0500 Subject: [PATCH 077/118] packaging: 4.0.0-alpha.3 --- packages/graphql/CHANGELOG.md | 5 +++++ packages/graphql/pubspec.yaml | 2 +- packages/graphql_flutter/CHANGELOG.md | 5 +++++ packages/graphql_flutter/pubspec.yaml | 4 ++-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 5f7327a89..ae83c1650 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.3 (2020-07-27) +* **client**: don't close mutations after callbacks ([2ba6c74](https://github.com/zino-app/graphql-flutter/commit/2ba6c743a7317c3df05c3f1c5e8e3e3cd44d6827)) +* **examples**: update ios files for graphql_flutter/example ([5b6e3d0](https://github.com/zino-app/graphql-flutter/commit/5b6e3d06a7dc56888dcbfc4c395ea51985c10f1c)) + + # 4.0.0-alpha.2 (2020-07-24) * **client**: simplified AuthLink ([0b3fbd9](https://github.com/zino-app/graphql-flutter/commit/0b3fbd9a4d3d0f9bded2bd9a9fdf26e3bfe983df)) * **docs**: direct cache access in changelog ([1f21927](https://github.com/zino-app/graphql-flutter/commit/1f2192710531a920be6c7df1b22da105c8cdc11c)) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 6f10abda8..dfb6d4f62 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.2 +version: 4.0.0-alpha.3 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index b450b8261..354bbf52f 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.3 (2020-07-27) +* **client**: don't close mutations after callbacks ([2ba6c74](https://github.com/zino-app/graphql-flutter/commit/2ba6c743a7317c3df05c3f1c5e8e3e3cd44d6827)) +* **examples**: update ios files for graphql_flutter/example ([5b6e3d0](https://github.com/zino-app/graphql-flutter/commit/5b6e3d06a7dc56888dcbfc4c395ea51985c10f1c)) + + # 4.0.0-alpha.2 (2020-06-17) * **client**: simplified AuthLink ([0b3fbd9](https://github.com/zino-app/graphql-flutter/commit/0b3fbd9a4d3d0f9bded2bd9a9fdf26e3bfe983df)) * **docs**: direct cache access in changelog ([1f21927](https://github.com/zino-app/graphql-flutter/commit/1f2192710531a920be6c7df1b22da105c8cdc11c)) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 7526c1de2..662a0236d 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,10 +2,10 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.2 +version: 4.0.0-alpha.3 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.2 + graphql: ^4.0.0-alpha.3 #path: ../graphql gql_exec: ^0.2.2 flutter: From d1d95fdef1156064014f59fd0843e1878fa481e0 Mon Sep 17 00:00:00 2001 From: micimize Date: Tue, 11 Aug 2020 23:05:59 -0500 Subject: [PATCH 078/118] can add a goal --- examples/starwars/pubspec.yaml | 7 +++++-- packages/graphql/pubspec.yaml | 4 ++-- .../graphql_flutter/lib/src/widgets/subscription.dart | 8 ++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/starwars/pubspec.yaml b/examples/starwars/pubspec.yaml index e58346095..1d52a149c 100644 --- a/examples/starwars/pubspec.yaml +++ b/examples/starwars/pubspec.yaml @@ -5,9 +5,7 @@ dependencies: flutter: sdk: flutter graphql_flutter: - path: ../../packages/graphql_flutter graphql: - path: ../../packages/graphql universal_platform: ^0.1.3 # https://github.com/flutter/flutter/issues/36126#issuecomment-596215587 @@ -17,6 +15,11 @@ flutter: uses-material-design: true dependency_overrides: + graphql_flutter: + path: ../../packages/graphql_flutter + graphql: + path: ../../packages/graphql + graphql_server: git: url: git@github.com:micimize/angel.git diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index dfb6d4f62..da28ceab8 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -11,8 +11,8 @@ dependencies: gql: ^0.12.0 gql_exec: ^0.2.2 gql_link: ^0.3.0 - gql_http_link: ^0.2.11-alpha+1594319799546 - gql_websocket_link: ^0.1.1-alpha + gql_http_link: ^0.2.11-alpha + gql_websocket_link: ^0.1.1-alpha+1596210062684 gql_transform_link: ^0.1.5 gql_error_link: ^0.1.1-alpha gql_dedupe_link: ^1.0.10 diff --git a/packages/graphql_flutter/lib/src/widgets/subscription.dart b/packages/graphql_flutter/lib/src/widgets/subscription.dart index fbaea9c60..ada783c9a 100644 --- a/packages/graphql_flutter/lib/src/widgets/subscription.dart +++ b/packages/graphql_flutter/lib/src/widgets/subscription.dart @@ -64,7 +64,7 @@ typedef SubscriptionBuilder = Widget Function(QueryResult result); /// } /// ``` /// {@end-tool} -class Subscription extends StatefulWidget { +class Subscription extends StatefulWidget { const Subscription({ @required this.options, @required this.builder, @@ -77,10 +77,10 @@ class Subscription extends StatefulWidget { final OnSubscriptionResult onSubscriptionResult; @override - _SubscriptionState createState() => _SubscriptionState(); + _SubscriptionState createState() => _SubscriptionState(); } -class _SubscriptionState extends State> { +class _SubscriptionState extends State { Stream stream; GraphQLClient client; @@ -118,7 +118,7 @@ class _SubscriptionState extends State> { } @override - void didUpdateWidget(Subscription oldWidget) { + void didUpdateWidget(Subscription oldWidget) { super.didUpdateWidget(oldWidget); if (!widget.options.equal(oldWidget.options)) { From 496d994e06148fbad1a394c7b3d68e43a8e8acaf Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 5 Sep 2020 14:19:48 -0500 Subject: [PATCH 079/118] feat(graphql): update old websocket_link --- packages/graphql/lib/src/links/gql_links.dart | 2 +- packages/graphql/lib/src/links/links.dart | 2 + .../websocket_link/websocket_client.dart | 447 ++++++++++++++++++ .../links/websocket_link/websocket_link.dart | 51 ++ .../websocket_link/websocket_messages.dart | 228 +++++++++ 5 files changed, 729 insertions(+), 1 deletion(-) create mode 100644 packages/graphql/lib/src/links/websocket_link/websocket_client.dart create mode 100644 packages/graphql/lib/src/links/websocket_link/websocket_link.dart create mode 100644 packages/graphql/lib/src/links/websocket_link/websocket_messages.dart diff --git a/packages/graphql/lib/src/links/gql_links.dart b/packages/graphql/lib/src/links/gql_links.dart index 9b5a32a2c..715f91660 100644 --- a/packages/graphql/lib/src/links/gql_links.dart +++ b/packages/graphql/lib/src/links/gql_links.dart @@ -1,6 +1,6 @@ export 'package:gql_link/gql_link.dart'; export 'package:gql_http_link/gql_http_link.dart'; -export 'package:gql_websocket_link/gql_websocket_link.dart'; +// export 'package:gql_websocket_link/gql_websocket_link.dart'; export 'package:gql_error_link/gql_error_link.dart'; export 'package:gql_dedupe_link/gql_dedupe_link.dart'; diff --git a/packages/graphql/lib/src/links/links.dart b/packages/graphql/lib/src/links/links.dart index ce0e0a0b2..1ead5ab31 100644 --- a/packages/graphql/lib/src/links/links.dart +++ b/packages/graphql/lib/src/links/links.dart @@ -2,3 +2,5 @@ export 'package:graphql/src/links/gql_links.dart'; export 'package:graphql/src/links/auth_link.dart'; + +export 'package:graphql/src/links/websocket_link/websocket_link.dart'; diff --git a/packages/graphql/lib/src/links/websocket_link/websocket_client.dart b/packages/graphql/lib/src/links/websocket_link/websocket_client.dart new file mode 100644 index 000000000..ac649ac2e --- /dev/null +++ b/packages/graphql/lib/src/links/websocket_link/websocket_client.dart @@ -0,0 +1,447 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:graphql/src/links/gql_links.dart'; +import 'package:meta/meta.dart'; + +import 'package:graphql/src/core/query_options.dart' show WithType; +import 'package:gql_exec/gql_exec.dart'; +import 'package:websocket/websocket.dart' show WebSocket, WebSocketStatus; + +import 'package:rxdart/rxdart.dart'; +import 'package:uuid_enhanced/uuid.dart'; + +import './websocket_messages.dart'; + +typedef GetInitPayload = FutureOr Function(); + +class SubscriptionListener { + Function callback; + bool hasBeenTriggered = false; + + SubscriptionListener(this.callback, this.hasBeenTriggered); +} + +class SocketClientConfig { + const SocketClientConfig({ + this.serializer = const RequestSerializer(), + this.parser = const ResponseParser(), + this.autoReconnect = true, + this.queryAndMutationTimeout = const Duration(seconds: 10), + this.inactivityTimeout = const Duration(seconds: 30), + this.delayBetweenReconnectionAttempts = const Duration(seconds: 5), + this.onConnectOrReconnect, + dynamic initialPayload, + @deprecated dynamic initPayload, + }) + // ignore: deprecated_member_use_from_same_package + : initialPayload = initialPayload ?? initPayload; + + /// Serializer used to serialize request + final RequestSerializer serializer; + + /// Response parser + final ResponseParser parser; + + /// Whether to reconnect to the server after detecting connection loss. + final bool autoReconnect; + + /// The duration after which the connection is considered unstable, because no keep alive message + /// was received from the server in the given time-frame. The connection to the server will be closed. + /// If [autoReconnect] is set to true, we try to reconnect to the server after the specified [delayBetweenReconnectionAttempts]. + /// + /// If null, the keep alive messages will be ignored. + final Duration inactivityTimeout; + + /// The duration that needs to pass before trying to reconnect to the server after a connection loss. + /// This only takes effect when [autoReconnect] is set to true. + /// + /// If null, the reconnection will occur immediately, although not recommended. + final Duration delayBetweenReconnectionAttempts; + + /// The duration after which a query or mutation should time out. + /// If null, no timeout is applied, although not recommended. + final Duration queryAndMutationTimeout; + + /// Callback for handling connections and reconnections. + /// + /// Useful for registering custom listeners or extracting the socket for other non-graphql features. + final void Function(WebSocket socket) onConnectOrReconnect; + + /// Payload to be sent with the connection_init request. + /// + /// Can be a literal value, a callback, or an async callback. End value must be valid argument for `json.encode`. + /// + /// Internal usage is roughly: + /// ```dart + /// Future get initOperation async { + /// if (initialPayload is Function) { + /// final dynamic payload = await initialPayload(); + /// return InitOperation(payload); + /// } else { + /// return InitOperation(initialPayload); + /// } + /// } + /// ``` + final dynamic initialPayload; + + Future get initOperation async { + if (initialPayload is Function) { + final dynamic payload = await initialPayload(); + return InitOperation(payload); + } else { + return InitOperation(initialPayload); + } + } +} + +enum SocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED } + +/// Wraps a standard web socket instance to marshal and un-marshal the server / +/// client payloads into dart object representation. +/// +/// This class also deals with reconnection, handles timeout and keep alive messages. +/// +/// It is meant to be instantiated once, and you can let this class handle all the heavy- +/// lifting of socket state management. Once you're done with the socket connection, make sure +/// you call the [dispose] method to release all allocated resources. +class SocketClient { + SocketClient( + this.url, { + this.protocols = const [ + 'graphql-ws', + ], + this.config = const SocketClientConfig(), + @visibleForTesting this.randomBytesForUuid, + }) { + _connect(); + } + + Uint8List randomBytesForUuid; + final String url; + final SocketClientConfig config; + final Iterable protocols; + final BehaviorSubject _connectionStateController = + BehaviorSubject(); + + final HashMap _subscriptionInitializers = + HashMap(); + bool _connectionWasLost = false; + + Timer _reconnectTimer; + WebSocket _socket; + + Stream _messageStream; + + StreamSubscription _keepAliveSubscription; + StreamSubscription _messageSubscription; + + Map Function(Request) get serialize => + config.serializer.serializeRequest; + Response Function(Map) get parse => + config.parser.parseResponse; + + /// Connects to the server. + /// + /// If this instance is disposed, this method does nothing. + Future _connect() async { + final InitOperation initOperation = await config.initOperation; + + if (_connectionStateController.isClosed) { + return; + } + + _connectionStateController.value = SocketConnectionState.CONNECTING; + print('Connecting to websocket: $url...'); + + try { + _socket = await WebSocket.connect( + url, + protocols: protocols, + ); + _connectionStateController.value = SocketConnectionState.CONNECTED; + print('Connected to websocket.'); + _write(initOperation); + + _messageStream = + _socket.stream.map(_parseSocketMessage); + + if (config.inactivityTimeout != null) { + _keepAliveSubscription = _messagesOfType().timeout( + config.inactivityTimeout, + onTimeout: (EventSink event) { + print( + "Haven't received keep alive message for ${config.inactivityTimeout.inSeconds} seconds. Disconnecting.."); + event.close(); + _socket.close(WebSocketStatus.goingAway); + _connectionStateController.value = + SocketConnectionState.NOT_CONNECTED; + }, + ).listen(null); + } + + _messageSubscription = _messageStream.listen( + (dynamic data) { + // print('data: $data'); + }, + onDone: () { + // print('done'); + onConnectionLost(); + }, + cancelOnError: true, + onError: (dynamic e) { + print('error: $e'); + }); + + if (_connectionWasLost) { + for (SubscriptionListener s in _subscriptionInitializers.values) { + s.callback(); + } + + _connectionWasLost = false; + } + + if (config.onConnectOrReconnect != null) { + config.onConnectOrReconnect(_socket); + } + } catch (e) { + onConnectionLost(e); + } + } + + void onConnectionLost([e]) { + if (e != null) { + print('There was an error causing connection lost: $e'); + } + print('Disconnected from websocket.'); + _reconnectTimer?.cancel(); + _keepAliveSubscription?.cancel(); + _messageSubscription?.cancel(); + + if (_connectionStateController.isClosed) { + return; + } + + _connectionWasLost = true; + _subscriptionInitializers.values.forEach((s) => s.hasBeenTriggered = false); + + if (_connectionStateController.value != + SocketConnectionState.NOT_CONNECTED) { + _connectionStateController.value = SocketConnectionState.NOT_CONNECTED; + } + + if (config.autoReconnect && !_connectionStateController.isClosed) { + if (config.delayBetweenReconnectionAttempts != null) { + print( + 'Scheduling to connect in ${config.delayBetweenReconnectionAttempts.inSeconds} seconds...'); + + _reconnectTimer = Timer( + config.delayBetweenReconnectionAttempts, + () { + _connect(); + }, + ); + } else { + Timer.run(() => _connect()); + } + } + } + + /// Closes the underlying socket if connected, and stops reconnection attempts. + /// After calling this method, this [SocketClient] instance must be considered + /// unusable. Instead, create a new instance of this class. + /// + /// Use this method if you'd like to disconnect from the specified server permanently, + /// and you'd like to connect to another server instead of the current one. + Future dispose() async { + print('Disposing socket client..'); + _reconnectTimer?.cancel(); + await Future.wait([ + _socket?.close(), + _keepAliveSubscription?.cancel(), + _messageSubscription?.cancel(), + _connectionStateController?.close(), + ]); + } + + static GraphQLSocketMessage _parseSocketMessage(dynamic message) { + final Map map = + json.decode(message as String) as Map; + final String type = (map['type'] ?? 'unknown') as String; + final dynamic payload = map['payload'] ?? {}; + final String id = (map['id'] ?? 'none') as String; + + switch (type) { + case MessageTypes.connectionAck: + return ConnectionAck(); + case MessageTypes.connectionError: + return ConnectionError(payload); + case MessageTypes.connectionKeepAlive: + return ConnectionKeepAlive(); + case MessageTypes.data: + final dynamic data = payload['data']; + final dynamic errors = payload['errors']; + return SubscriptionData(id, data, errors); + case MessageTypes.error: + return SubscriptionError(id, payload); + case MessageTypes.complete: + return SubscriptionComplete(id); + default: + return UnknownData(map); + } + } + + void _write(final GraphQLSocketMessage message) { + if (_connectionStateController.value == SocketConnectionState.CONNECTED) { + _socket.add( + json.encode( + message, + toEncodable: (dynamic m) => m.toJson(), + ), + ); + } + } + + /// Sends a query, mutation or subscription request to the server, and returns a stream of the response. + /// + /// If the request is a query or mutation, a timeout will be applied to the request as specified by + /// [SocketClientConfig]'s [queryAndMutationTimeout] field. + /// + /// If the request is a subscription, obviously no timeout is applied. + /// + /// In case of socket disconnection, the returned stream will be closed. + Stream subscribe( + final Request payload, + final bool waitForConnection, + ) { + final String id = Uuid.randomUuid(random: randomBytesForUuid).toString(); + final StreamController response = StreamController(); + StreamSubscription sub; + final bool addTimeout = + !payload.isSubscription && config.queryAndMutationTimeout != null; + + final onListen = () { + final Stream waitForConnectedStateWithoutTimeout = + _connectionStateController + .startWith( + waitForConnection ? null : SocketConnectionState.CONNECTED) + .where((SocketConnectionState state) => + state == SocketConnectionState.CONNECTED) + .take(1); + + final Stream waitForConnectedState = addTimeout + ? waitForConnectedStateWithoutTimeout.timeout( + config.queryAndMutationTimeout, + onTimeout: (EventSink event) { + print('Connection timed out.'); + response.addError(TimeoutException('Connection timed out.')); + event.close(); + response.close(); + }, + ) + : waitForConnectedStateWithoutTimeout; + + sub = waitForConnectedState.listen((_) { + final Stream dataErrorComplete = + _messageStream.where( + (GraphQLSocketMessage message) { + if (message is SubscriptionData) { + return message.id == id; + } + + if (message is SubscriptionError) { + return message.id == id; + } + + if (message is SubscriptionComplete) { + return message.id == id; + } + + return false; + }, + ).takeWhile((_) => !response.isClosed); + + final Stream subscriptionComplete = addTimeout + ? dataErrorComplete + .where((GraphQLSocketMessage message) => + message is SubscriptionComplete) + .take(1) + .timeout( + config.queryAndMutationTimeout, + onTimeout: (EventSink event) { + print('Request timed out.'); + response.addError(TimeoutException('Request timed out.')); + event.close(); + response.close(); + }, + ) + : dataErrorComplete + .where((GraphQLSocketMessage message) => + message is SubscriptionComplete) + .take(1); + + subscriptionComplete.listen((_) => response.close()); + + dataErrorComplete + .where( + (GraphQLSocketMessage message) => message is SubscriptionData) + .cast() + .listen( + (SubscriptionData message) => response.add( + parse( + message.toJson(), + ), + ), + ); + + dataErrorComplete + .where( + (GraphQLSocketMessage message) => message is SubscriptionError) + .listen( + (GraphQLSocketMessage message) => response.addError(message)); + + if (!_subscriptionInitializers[id].hasBeenTriggered) { + _write( + StartOperation( + id, + serialize(payload), + ), + ); + _subscriptionInitializers[id].hasBeenTriggered = true; + } + }); + }; + + response.onListen = onListen; + + response.onCancel = () { + _subscriptionInitializers.remove(id); + + sub?.cancel(); + if (_connectionStateController.value == SocketConnectionState.CONNECTED && + _socket != null) { + _write(StopOperation(id)); + } + }; + + _subscriptionInitializers[id] = SubscriptionListener(onListen, false); + + return response.stream; + } + + /// These streams will emit done events when the current socket is done. + /// A stream that emits the last value of the connection state upon subscription. + Stream get connectionState => + _connectionStateController.stream; + + /// Filter `_messageStream` for messages of the given type of [GraphQLSocketMessage] + /// + /// Example usages: + /// `_messagesOfType()` for init acknowledgments + /// `_messagesOfType()` for errors + /// `_messagesOfType()` for unknown data messages + Stream _messagesOfType() => _messageStream + .where((GraphQLSocketMessage message) => message is M) + .cast(); +} diff --git a/packages/graphql/lib/src/links/websocket_link/websocket_link.dart b/packages/graphql/lib/src/links/websocket_link/websocket_link.dart new file mode 100644 index 000000000..814d7bbe2 --- /dev/null +++ b/packages/graphql/lib/src/links/websocket_link/websocket_link.dart @@ -0,0 +1,51 @@ +import 'package:gql_link/gql_link.dart'; +import 'package:gql_exec/gql_exec.dart'; + +import './websocket_client.dart'; + +export './websocket_client.dart'; +export './websocket_messages.dart'; + +/// A Universal Websocket [Link] implementation to support the websocket transport. +/// It supports subscriptions, query and mutation operations as well. +/// +/// NOTE: the actual socket connection will only get established after a [Request] is handled by this [WebSocketLink]. +/// If you'd like to connect to the socket server instantly, call the [connectOrReconnect] method after creating this [WebSocketLink] instance. +class WebSocketLink extends Link { + /// Creates a new [WebSocketLink] instance with the specified config. + WebSocketLink( + this.url, { + this.config = const SocketClientConfig(), + }); + + final String url; + final SocketClientConfig config; + + // cannot be final because we're changing the instance upon a header change. + SocketClient _socketClient; + + @override + Stream request(Request request, [forward]) async* { + if (_socketClient == null) { + connectOrReconnect(); + } + + yield* _socketClient.subscribe(request, true); + } + + /// Connects or reconnects to the server with the specified headers. + void connectOrReconnect() { + _socketClient?.dispose(); + _socketClient = SocketClient( + url, + config: config, + ); + } + + /// Disposes the underlying socket client explicitly. Only use this, if you want to disconnect from + /// the current server in favour of another one. If that's the case, create a new [WebSocketLink] instance. + Future dispose() async { + await _socketClient?.dispose(); + _socketClient = null; + } +} diff --git a/packages/graphql/lib/src/links/websocket_link/websocket_messages.dart b/packages/graphql/lib/src/links/websocket_link/websocket_messages.dart new file mode 100644 index 000000000..b53c796c9 --- /dev/null +++ b/packages/graphql/lib/src/links/websocket_link/websocket_messages.dart @@ -0,0 +1,228 @@ +// Adapted to `gql` by @iscriptology + +import "dart:convert"; +import "package:meta/meta.dart"; + +/// These messages represent the structures used for Client-server communication +/// in a GraphQL web-socket subscription. Each message is represented in a JSON +/// format where the data type is denoted by the `type` field. + +/// A list of constants used for identifying message types +class MessageTypes { + MessageTypes._(); + + // client connections + static const String connectionInit = "connection_init"; + static const String connectionTerminate = "connection_terminate"; + + // server connections + static const String connectionAck = "connection_ack"; + static const String connectionError = "connection_error"; + static const String connectionKeepAlive = "ka"; + + // client operations + static const String start = "start"; + static const String stop = "stop"; + + // server operations + static const String data = "data"; + static const String error = "error"; + static const String complete = "complete"; + + // default tag for use in identifying issues + static const String unknown = "unknown"; +} + +abstract class JsonSerializable { + Map toJson(); + + @override + String toString() => toJson().toString(); +} + +/// Base type for representing a server-client subscription message. +abstract class GraphQLSocketMessage extends JsonSerializable { + GraphQLSocketMessage(this.type); + + final String type; +} + +/// After establishing a connection with the server, the client will +/// send this message to tell the server that it is ready to begin sending +/// new subscription queries. +class InitOperation extends GraphQLSocketMessage { + InitOperation(this.payload) : super(MessageTypes.connectionInit); + + final dynamic payload; + + @override + Map toJson() { + final Map jsonMap = {}; + jsonMap["type"] = type; + + if (payload != null) { + jsonMap["payload"] = payload; + } + + return jsonMap; + } +} + +/// Represent the payload used during a Start query operation. +/// The operationName should match one of the top level query definitions +/// defined in the query provided. Additional variables can be provided +/// and sent to the server for processing. +class QueryPayload extends JsonSerializable { + QueryPayload( + {this.operationName, @required this.query, @required this.variables}); + + final String operationName; + final String query; + final Map variables; + + @override + Map toJson() => { + "operationName": operationName, + "query": query, + "variables": variables, + }; +} + +/// A message to tell the server to create a subscription. The contents of the +/// query will be defined by the payload request. The id provided will be used +/// to tag messages such that they can be identified for this subscription +/// instance. id values should be unique and not be re-used during the lifetime +/// of the server. +class StartOperation extends GraphQLSocketMessage { + StartOperation(this.id, this.payload) : super(MessageTypes.start); + + final String id; +// final QueryPayload payload; + final Map payload; + + @override + Map toJson() => { + "type": type, + "id": id, + "payload": payload, + }; +} + +/// Tell the server to stop sending subscription data for a particular +/// subscription instance. See [StartOperation]. +class StopOperation extends GraphQLSocketMessage { + StopOperation(this.id) : super(MessageTypes.stop); + + final String id; + + @override + Map toJson() => { + "type": type, + "id": id, + }; +} + +/// The server will send this acknowledgment message after receiving the init +/// command from the client if the init was successful. +class ConnectionAck extends GraphQLSocketMessage { + ConnectionAck() : super(MessageTypes.connectionAck); + + @override + Map toJson() => { + "type": type, + }; +} + +/// The server will send this error message after receiving the init command +/// from the client if the init was not successful. +class ConnectionError extends GraphQLSocketMessage { + ConnectionError(this.payload) : super(MessageTypes.connectionError); + + final dynamic payload; + + @override + Map toJson() => { + "type": type, + "payload": payload, + }; +} + +/// The server will send this message to keep the connection alive +class ConnectionKeepAlive extends GraphQLSocketMessage { + ConnectionKeepAlive() : super(MessageTypes.connectionKeepAlive); + + @override + Map toJson() => { + "type": type, + }; +} + +/// Data sent from the server to the client with subscription data or error +/// payload. The user should check the errors result before processing the +/// data value. These error are from the query resolvers. +class SubscriptionData extends GraphQLSocketMessage { + SubscriptionData(this.id, this.data, this.errors) : super(MessageTypes.data); + + final String id; + final dynamic data; + final dynamic errors; + + @override + Map toJson() => { + "type": type, + "data": data, + "errors": errors, + }; + + @override + int get hashCode => toJson().hashCode; + + @override + bool operator ==(dynamic other) => + other is SubscriptionData && jsonEncode(other) == jsonEncode(this); +} + +/// Errors sent from the server to the client if the subscription operation was +/// not successful, usually due to GraphQL validation errors. +class SubscriptionError extends GraphQLSocketMessage { + SubscriptionError(this.id, this.payload) : super(MessageTypes.error); + + final String id; + final dynamic payload; + + @override + Map toJson() => { + "type": type, + "id": id, + "payload": payload, + }; +} + +/// Server message to the client to indicate that no more data will be sent +/// for a particular subscription instance. +class SubscriptionComplete extends GraphQLSocketMessage { + SubscriptionComplete(this.id) : super(MessageTypes.complete); + + final String id; + + @override + Map toJson() => { + "type": type, + "id": id, + }; +} + +/// Not expected to be created. Indicates there are problems parsing the server +/// response, or that new unsupported types have been added to the subscription +/// implementation. +class UnknownData extends GraphQLSocketMessage { + UnknownData(this.payload) : super(MessageTypes.unknown); + + final dynamic payload; + + @override + Map toJson() => { + "type": type, + "payload": payload, + }; +} From c2733ca3d33b1b50afc5b2ef7809fd1f4aa41500 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 5 Sep 2020 14:20:51 -0500 Subject: [PATCH 080/118] feat(graphql): multipart file support --- packages/graphql/pubspec.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index da28ceab8..ed6343176 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,17 +2,17 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.3 +version: 4.0.0-alpha.4 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 path: ^1.6.2 gql: ^0.12.0 - gql_exec: ^0.2.2 + gql_exec: ^0.2.4 gql_link: ^0.3.0 - gql_http_link: ^0.2.11-alpha - gql_websocket_link: ^0.1.1-alpha+1596210062684 + gql_http_link: ^0.3.0 + #gql_websocket_link: ^0.1.1-alpha+1596210062684 gql_transform_link: ^0.1.5 gql_error_link: ^0.1.1-alpha gql_dedupe_link: ^1.0.10 @@ -22,6 +22,8 @@ dependencies: http: ^0.12.1 collection: ^1.14.12 + websocket: ^0.0.5 + dev_dependencies: pedantic: ^1.8.0+1 mockito: ^4.0.0 From 27bca464e165dfa27273df07f315f7fc4978ca6b Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 5 Sep 2020 14:22:41 -0500 Subject: [PATCH 081/118] chore(packaging): rxdart and uuid_enhanced for old websocket --- packages/graphql/pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index ed6343176..5b66b920c 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -23,6 +23,8 @@ dependencies: collection: ^1.14.12 websocket: ^0.0.5 + rxdart: ^0.24.1 + uuid_enhanced: ^3.0.2 dev_dependencies: pedantic: ^1.8.0+1 From c3d7764c15e48576fde7fb4b74d6c7810c6e7a70 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 5 Sep 2020 14:26:08 -0500 Subject: [PATCH 082/118] chore(changelog): update changelog --- packages/graphql/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index ae83c1650..0ea683ef9 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.4 (2020-07-27) +* **client**: bring back old websocket link with gql adapter layer ([496d994](https://github.com/zino-app/graphql-flutter/commit/496d994e06148fbad1a394c7b3d68e43a8e8acaf)) +* **client**: multipart file support from `gql_http_link==0.3.0` ([c2733ca](https://github.com/zino-app/graphql-flutter/commit/c2733ca3d33b1b50afc5b2ef7809fd1f4aa41500)) + + # 4.0.0-alpha.3 (2020-07-27) * **client**: don't close mutations after callbacks ([2ba6c74](https://github.com/zino-app/graphql-flutter/commit/2ba6c743a7317c3df05c3f1c5e8e3e3cd44d6827)) * **examples**: update ios files for graphql_flutter/example ([5b6e3d0](https://github.com/zino-app/graphql-flutter/commit/5b6e3d06a7dc56888dcbfc4c395ea51985c10f1c)) From 803a2c69153833fc570d4c0f157f46af97fce91f Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 5 Sep 2020 14:27:47 -0500 Subject: [PATCH 083/118] packaging(graphql_flutter): 4.0.0-alpha.4 --- packages/graphql/CHANGELOG.md | 2 +- packages/graphql_flutter/CHANGELOG.md | 5 +++++ packages/graphql_flutter/pubspec.yaml | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 0ea683ef9..187c4d381 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,4 +1,4 @@ -# 4.0.0-alpha.4 (2020-07-27) +# 4.0.0-alpha.4 (2020-09-05) * **client**: bring back old websocket link with gql adapter layer ([496d994](https://github.com/zino-app/graphql-flutter/commit/496d994e06148fbad1a394c7b3d68e43a8e8acaf)) * **client**: multipart file support from `gql_http_link==0.3.0` ([c2733ca](https://github.com/zino-app/graphql-flutter/commit/c2733ca3d33b1b50afc5b2ef7809fd1f4aa41500)) diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index 354bbf52f..545488c12 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.4 (2020-09-05) +* **client**: bring back old websocket link with gql adapter layer ([496d994](https://github.com/zino-app/graphql-flutter/commit/496d994e06148fbad1a394c7b3d68e43a8e8acaf)) +* **client**: multipart file support from `gql_http_link==0.3.0` ([c2733ca](https://github.com/zino-app/graphql-flutter/commit/c2733ca3d33b1b50afc5b2ef7809fd1f4aa41500)) + + # 4.0.0-alpha.3 (2020-07-27) * **client**: don't close mutations after callbacks ([2ba6c74](https://github.com/zino-app/graphql-flutter/commit/2ba6c743a7317c3df05c3f1c5e8e3e3cd44d6827)) * **examples**: update ios files for graphql_flutter/example ([5b6e3d0](https://github.com/zino-app/graphql-flutter/commit/5b6e3d06a7dc56888dcbfc4c395ea51985c10f1c)) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 662a0236d..80666fb23 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,10 +2,10 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.3 +version: 4.0.0-alpha.4 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.3 + graphql: ^4.0.0-alpha.4 #path: ../graphql gql_exec: ^0.2.2 flutter: From 4ceb8006baf4539ab423e3b3a229f194cc2eac45 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 12 Sep 2020 12:21:57 -0500 Subject: [PATCH 084/118] fix(graphql): sanitize multipart files for cache --- .../src/cache/_normalizing_data_proxy.dart | 13 ++-- .../src/cache/_optimistic_transactions.dart | 3 + packages/graphql/lib/src/cache/cache.dart | 14 ++++- .../graphql/lib/src/utilities/helpers.dart | 31 ++++++++++ .../graphql/lib/src/utilities/traverse.dart | 62 ------------------- packages/graphql/test/cache/cache_data.dart | 48 ++++++++++++++ .../test/cache/graphql_cache_test.dart | 12 ++++ 7 files changed, 116 insertions(+), 67 deletions(-) delete mode 100644 packages/graphql/lib/src/utilities/traverse.dart diff --git a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart index e41dc3521..3e45ac50f 100644 --- a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart @@ -6,6 +6,7 @@ import 'package:gql/ast.dart' show DocumentNode; import 'package:normalize/normalize.dart'; import './data_proxy.dart'; +import '../utilities/helpers.dart'; typedef DataIdResolver = String Function(Map object); @@ -53,6 +54,10 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { @protected void writeNormalized(String dataId, dynamic value); + /// Variable sanitizer for referencing custom scalar types in cache keys. + @protected + SanitizeVariables sanitizeVariables; + Map readQuery( Request request, { bool optimistic = true, @@ -61,7 +66,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { reader: (dataId) => readNormalized(dataId, optimistic: optimistic), query: request.operation.document, operationName: request.operation.operationName, - variables: request.variables, + variables: sanitizeVariables(request.variables), typePolicies: typePolicies, addTypename: addTypename ?? false, returnPartialData: returnPartialData, @@ -79,7 +84,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { fragment: fragment, idFields: idFields, fragmentName: fragmentName, - variables: variables, + variables: sanitizeVariables(variables), typePolicies: typePolicies, addTypename: addTypename ?? false, dataIdFromObject: dataIdFromObject, @@ -95,7 +100,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { writer: (dataId, value) => writeNormalized(dataId, value), query: request.operation.document, operationName: request.operation.operationName, - variables: request.variables, + variables: sanitizeVariables(request.variables), data: data, typePolicies: typePolicies, dataIdFromObject: dataIdFromObject, @@ -119,7 +124,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { idFields: idFields, data: data, fragmentName: fragmentName, - variables: variables, + variables: sanitizeVariables(variables), typePolicies: typePolicies, dataIdFromObject: dataIdFromObject, ); diff --git a/packages/graphql/lib/src/cache/_optimistic_transactions.dart b/packages/graphql/lib/src/cache/_optimistic_transactions.dart index 52538c10b..ef8b0974c 100644 --- a/packages/graphql/lib/src/cache/_optimistic_transactions.dart +++ b/packages/graphql/lib/src/cache/_optimistic_transactions.dart @@ -32,6 +32,9 @@ class OptimisticProxy extends NormalizingDataProxy { GraphQLCache cache; + @override + SanitizeVariables get sanitizeVariables => cache.sanitizeVariables; + HashMap data = HashMap(); @override diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index 88fc73ada..2da930d59 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -11,6 +11,8 @@ export 'package:graphql/src/cache/data_proxy.dart'; export 'package:graphql/src/cache/store.dart'; export 'package:graphql/src/cache/hive_store.dart'; +typedef VariableEncoder = Object Function(Object t); + /// Optimmistic GraphQL Entity cache with [normalize] [TypePolicy] support /// and configurable [store]. /// @@ -21,7 +23,14 @@ class GraphQLCache extends NormalizingDataProxy { Store store, this.dataIdFromObject, this.typePolicies = const {}, - }) : store = store ?? InMemoryStore(); + + /// Input variable sanitizer for referencing custom scalar types in cache keys. + /// + /// Defaults to [sanitizeFilesForCache]. Can be set to `null` to disable sanitization. + /// If present, a sanitizer will be built with [variableSanitizer] + Object Function(Object) sanitizeVariables = sanitizeFilesForCache, + }) : sanitizeVariables = variableSanitizer(sanitizeVariables), + store = store ?? InMemoryStore(); /// Stores the underlying normalized data. Defaults to an [InMemoryStore] @protected @@ -33,6 +42,9 @@ class GraphQLCache extends NormalizingDataProxy { /// Optional `dataIdFromObject` function to pass through to [normalize] final DataIdResolver dataIdFromObject; + @override + final SanitizeVariables sanitizeVariables; + /// tracks the number of ongoing transactions to prevent /// rebroadcasts until they are completed @protected diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index 4c1d0b9af..39fccc825 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; + import 'package:gql/ast.dart'; import 'package:gql/language.dart'; +import 'package:http/http.dart' show MultipartFile; import 'package:normalize/normalize.dart'; bool notNull(Object any) { @@ -60,3 +63,31 @@ DocumentNode gql(String document) => transform( AddTypenameVisitor(), ], ); + +/// Convets [MultipartFile]s to a string representation containing hashCode. Default argument to [variableSanitizer] +Object sanitizeFilesForCache(dynamic object) { + if (object is MultipartFile) { + return 'MultipartFile(filename=${object.filename} hashCode=${object.hashCode})'; + } + return object.toJson(); +} + +typedef SanitizeVariables = Map Function( + Map variables, +); + +/// Build a sanitizer for safely writing custom scalar inputs in variable arguments to the cache. +/// +/// [sanitizeVariables] is passed to [jsonEncode] as `toEncodable`. The default is [defaultSanitizeVariables], +/// which convets [MultipartFile]s to a string representation containing hashCode) +SanitizeVariables variableSanitizer( + Object Function(Object) sanitizeVariables, +) => + sanitizeVariables == null + ? (v) => v + : (variables) => jsonDecode( + jsonEncode( + variables, + toEncodable: sanitizeVariables, + ), + ); diff --git a/packages/graphql/lib/src/utilities/traverse.dart b/packages/graphql/lib/src/utilities/traverse.dart deleted file mode 100644 index 63a13c2ef..000000000 --- a/packages/graphql/lib/src/utilities/traverse.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:collection'; - -typedef Transform = Object Function(Object node); -typedef SideEffect = void Function( - Object transformResult, - Object node, - Traversal traversal, -); - -class Traversal { - Traversal( - this.transform, { - this.transformSideEffect, - this.seenObjects, - }) { - seenObjects ??= HashSet(); - } - - Transform transform; - - /// An optional side effect to call when a node is transformed. - SideEffect transformSideEffect; - HashSet seenObjects; - - bool alreadySeen(Object node) { - final bool wasAdded = seenObjects.add(node); - return !wasAdded; - } - - /// Traverse only the values of the given map - Map traverseValues(Map node) { - return node.map( - (String key, Object value) => MapEntry( - key, - traverse(value), - ), - ); - } - - // Attempts to apply the transform to every leaf of the data structure recursively. - // Stops recursing when a node is transformed (returns non-null) - Object traverse(Object node) { - final Object transformed = transform(node); - if (alreadySeen(node)) { - return transformed ?? node; - } - if (transformed != null) { - if (transformSideEffect != null) { - transformSideEffect(transformed, node, this); - } - return transformed; - } - - if (node is List) { - return node.map((Object node) => traverse(node)).toList(); - } - if (node is Map) { - return traverseValues(node); - } - return node; - } -} diff --git a/packages/graphql/test/cache/cache_data.dart b/packages/graphql/test/cache/cache_data.dart index 029704e96..26ab3ecd0 100644 --- a/packages/graphql/test/cache/cache_data.dart +++ b/packages/graphql/test/cache/cache_data.dart @@ -2,6 +2,8 @@ import 'package:gql_exec/gql_exec.dart'; import 'package:gql/language.dart'; import 'package:graphql/src/utilities/helpers.dart'; import 'package:meta/meta.dart'; +import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; const String rawOperationKey = 'rawOperationKey'; @@ -83,6 +85,52 @@ final basicTest = TestCase( }, ); +/// https://github.com/gql-dart/gql/blob/master/links/gql_http_link/test/multipart_upload_test.dart +final fileVarsTest = TestCase( + data: { + "multipleUpload": [ + { + "id": "r1odc4PAz", + "filename": "sample_upload.jpg", + "mimetype": "image/jpeg", + "path": "./uploads/r1odc4PAz-sample_upload.jpg" + }, + { + "id": "5Ea18qlMur", + "filename": "sample_upload.txt", + "mimetype": "text/plain", + "path": "./uploads/5Ea18qlMur-sample_upload.txt" + } + ], + }, + operation: r""" + mutation($files: [Upload!]!) { + multipleUpload(files: $files) { + id + filename + mimetype + path + } + } + """, + variables: { + 'files': [ + http.MultipartFile.fromBytes( + "", + [0, 1, 254, 255], + filename: "sample_upload.jpg", + contentType: MediaType("image", "jpeg"), + ), + http.MultipartFile.fromString( + "", + "just plain text", + filename: "sample_upload.txt", + contentType: MediaType("text", "plain"), + ), + ], + }, +); + final updatedCFragment = parseString(r''' fragment partialC on C { __typename diff --git a/packages/graphql/test/cache/graphql_cache_test.dart b/packages/graphql/test/cache/graphql_cache_test.dart index af9d44842..f840118b7 100644 --- a/packages/graphql/test/cache/graphql_cache_test.dart +++ b/packages/graphql/test/cache/graphql_cache_test.dart @@ -1,3 +1,4 @@ +import 'package:http/http.dart'; import 'package:test/test.dart'; import 'package:graphql/src/cache/cache.dart'; @@ -140,4 +141,15 @@ void main() { ); }, ); + + group('Handles MultipartFile variables', () { + final GraphQLCache cache = getTestCache(); + test('.writeQuery .readQuery round trip', () { + cache.writeQuery(fileVarsTest.request, data: fileVarsTest.data); + expect( + cache.readQuery(fileVarsTest.request), + equals(fileVarsTest.data), + ); + }); + }); } From db1cdf74ddce8aae78ef081db0c35d0f7b5f9c2f Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 12 Sep 2020 12:27:38 -0500 Subject: [PATCH 085/118] 4.0.0-alpha.5 cache references for multipart files --- packages/graphql/CHANGELOG.md | 3 +++ packages/graphql/pubspec.yaml | 3 ++- packages/graphql_flutter/CHANGELOG.md | 3 +++ packages/graphql_flutter/pubspec.yaml | 4 ++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 187c4d381..2f443c10a 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,6 @@ +# 4.0.0-alpha.5 (2020-09-12) +* **cache**: sanitize multipart files for cache. ([4ceb800](https://github.com/zino-app/graphql-flutter/commit/4ceb8006baf4539ab423e3b3a229f194cc2eac45)) + # 4.0.0-alpha.4 (2020-09-05) * **client**: bring back old websocket link with gql adapter layer ([496d994](https://github.com/zino-app/graphql-flutter/commit/496d994e06148fbad1a394c7b3d68e43a8e8acaf)) * **client**: multipart file support from `gql_http_link==0.3.0` ([c2733ca](https://github.com/zino-app/graphql-flutter/commit/c2733ca3d33b1b50afc5b2ef7809fd1f4aa41500)) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 5b66b920c..7052fa6b2 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.4 +version: 4.0.0-alpha.5 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 @@ -31,6 +31,7 @@ dev_dependencies: mockito: ^4.0.0 test: ^1.5.3 test_coverage: ^0.3.0+1 + http_parser: ^3.1.4 environment: sdk: ">=2.6.0 <3.0.0" diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index 545488c12..ab6a2782e 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,6 @@ +# 4.0.0-alpha.5 (2020-09-12) +* **cache**: sanitize multipart files for cache. ([4ceb800](https://github.com/zino-app/graphql-flutter/commit/4ceb8006baf4539ab423e3b3a229f194cc2eac45)) + # 4.0.0-alpha.4 (2020-09-05) * **client**: bring back old websocket link with gql adapter layer ([496d994](https://github.com/zino-app/graphql-flutter/commit/496d994e06148fbad1a394c7b3d68e43a8e8acaf)) * **client**: multipart file support from `gql_http_link==0.3.0` ([c2733ca](https://github.com/zino-app/graphql-flutter/commit/c2733ca3d33b1b50afc5b2ef7809fd1f4aa41500)) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 80666fb23..deef59633 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,10 +2,10 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.4 +version: 4.0.0-alpha.5 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.4 + graphql: ^4.0.0-alpha.5 #path: ../graphql gql_exec: ^0.2.2 flutter: From 9c84cb13b7796b6b15027a8a2c76b00a953332bc Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 12 Sep 2020 12:34:01 -0500 Subject: [PATCH 086/118] fix(docs): typo in docstring, add todo to sanitizeVariables --- packages/graphql/lib/src/utilities/helpers.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/graphql/lib/src/utilities/helpers.dart b/packages/graphql/lib/src/utilities/helpers.dart index 39fccc825..24dc31a04 100644 --- a/packages/graphql/lib/src/utilities/helpers.dart +++ b/packages/graphql/lib/src/utilities/helpers.dart @@ -64,7 +64,7 @@ DocumentNode gql(String document) => transform( ], ); -/// Convets [MultipartFile]s to a string representation containing hashCode. Default argument to [variableSanitizer] +/// Converts [MultipartFile]s to a string representation containing hashCode. Default argument to [variableSanitizer] Object sanitizeFilesForCache(dynamic object) { if (object is MultipartFile) { return 'MultipartFile(filename=${object.filename} hashCode=${object.hashCode})'; @@ -83,6 +83,7 @@ typedef SanitizeVariables = Map Function( SanitizeVariables variableSanitizer( Object Function(Object) sanitizeVariables, ) => + // TODO use more efficient traversal method sanitizeVariables == null ? (v) => v : (variables) => jsonDecode( From 2067b46d99781b1514f47199ddd84145d609d808 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 12 Sep 2020 13:07:03 -0500 Subject: [PATCH 087/118] docs: add info on the new link system to readme --- packages/graphql/README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/graphql/README.md b/packages/graphql/README.md index c007c0c57..6a01c3211 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -74,6 +74,32 @@ final GraphQLClient _client = GraphQLClient( ### Combining Multiple Links +`graphql` and `graphql_flutter` now use the [`gql_link`] system, which allows for any kind of routing you might need: +![diagram](https://github.com/gql-dart/gql/blob/master/links/gql_link/assets/gql_link.svg) + +A quick rundown of the composition api: + +```dart +Link.from([ + // common links run before every request + AuthLink(getToken: commonAuthenticator), + DedupeLink(), // dedupe requests + ErrorLink(onException: reportClientException), +]).split( // split terminating links, or they will break + (request) => request.isSubscription, + MyCustomSubscriptionAuthLink().concat( + WebsocketLink(mySubscriptionEndpoint), + ), + HttpLink(myAppEndpoint), +); +// adding links after here would be pointless, as they would never be accessed +``` + +When combining links, it is important to note that: + +- Terminating links like `HttpLink` and `WebsocketLink` must come at the end of a route, and will not call links following them. +- Link order is very important. In `HttpLink(myEndpoint).concat(AuthLink(getToken: authenticate))`, the `AuthLink` will never be called. + #### Using Concat ```dart @@ -88,6 +114,15 @@ final Link _link = _authLink.concat(_httpLink); final Link _link = Link.from([_authLink, _httpLink]); ``` +#### Using Links.split + +`Link.split` routes the request based on some condition. +**NB**: `WebSocketLink` and other "terminating links" must be used with `split` when there are multiple. + +```dart +link = Link.split((request) => request.isSubscription, websocketLink, link); +``` + Once you have initialized a client, you can run queries and mutations. ### Query @@ -303,3 +338,4 @@ final Link _link = _apqLink.concat(_httpLink); [github-star-link]: https://github.com/zino-app/graphql-flutter/stargazers [discord-badge]: https://img.shields.io/discord/559455668810153989.svg?style=flat-square&logo=discord&logoColor=ffffff [discord-link]: https://discord.gg/tXTtBfC +[`gql_link`]: https://github.com/gql-dart/gql/tree/master/links/gql_link From de66cffab0c13f1d705bb4538f55a2e11b653a4f Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 12 Sep 2020 22:29:56 -0500 Subject: [PATCH 088/118] graphql: fix subscription initial result from cache --- packages/graphql/lib/src/core/query_manager.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index b0a73cd65..09ff5f1d7 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -57,7 +57,7 @@ class QueryManager { if (cacheResult != null) { yield QueryResult( source: QueryResultSource.cache, - data: options.optimisticResult, + data: cacheResult, ); } } From 7192c1669c7dbecff0bbcd0665522c5b41207ab2 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 12 Sep 2020 22:33:32 -0500 Subject: [PATCH 089/118] packaging: 4.0.0-alpha.6 subscription fix --- packages/graphql/CHANGELOG.md | 5 ++++- packages/graphql/pubspec.yaml | 2 +- packages/graphql_flutter/CHANGELOG.md | 3 +++ packages/graphql_flutter/pubspec.yaml | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 2f443c10a..ab0707f82 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,6 @@ +# 4.0.0-alpha.6 (2020-09-12) +* **client**: fix subscription initial result from cache ([de66cff](https://github.com/zino-app/graphql-flutter/commit/de66cffab0c13f1d705bb4538f55a2e11b653a4f)) + # 4.0.0-alpha.5 (2020-09-12) * **cache**: sanitize multipart files for cache. ([4ceb800](https://github.com/zino-app/graphql-flutter/commit/4ceb8006baf4539ab423e3b3a229f194cc2eac45)) @@ -15,7 +18,7 @@ * **client**: simplified AuthLink ([0b3fbd9](https://github.com/zino-app/graphql-flutter/commit/0b3fbd9a4d3d0f9bded2bd9a9fdf26e3bfe983df)) * **docs**: direct cache access in changelog ([1f21927](https://github.com/zino-app/graphql-flutter/commit/1f2192710531a920be6c7df1b22da105c8cdc11c)) * **examples**: starwars hivestore usage ([2f874ec](https://github.com/zino-app/graphql-flutter/commit/2f874ecde038e16332bb51243afb167ac0421e35)) -* **graphql**: `HiveStore` api improvements ([2d1a7f2](https://github.com/zino-app/graphql-flutter/commit/2d1a7f2e367f57f6ff2f968814045fb5edf15085)) +* **client**: `HiveStore` api improvements ([2d1a7f2](https://github.com/zino-app/graphql-flutter/commit/2d1a7f2e367f57f6ff2f968814045fb5edf15085)) * **fix**: `FetchMoreOptions` was throwing without `document` ([2d1a7f2](https://github.com/zino-app/graphql-flutter/commit/2d1a7f2e367f57f6ff2f968814045fb5edf15085)) * **fix**: `deeplyMergeLeft` type error ([65fdcb2](https://github.com/zino-app/graphql-flutter/commit/65fdcb2600257f8982496e5191424f42365f7f39)) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 7052fa6b2..0b95886ac 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.5 +version: 4.0.0-alpha.6 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index ab6a2782e..caf17b6ac 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,6 @@ +# 4.0.0-alpha.6 (2020-09-12) +* **client**: fix subscription initial result from cache ([de66cff](https://github.com/zino-app/graphql-flutter/commit/de66cffab0c13f1d705bb4538f55a2e11b653a4f)) + # 4.0.0-alpha.5 (2020-09-12) * **cache**: sanitize multipart files for cache. ([4ceb800](https://github.com/zino-app/graphql-flutter/commit/4ceb8006baf4539ab423e3b3a229f194cc2eac45)) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index deef59633..4af05e838 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,10 +2,10 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.5 +version: 4.0.0-alpha.6 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.5 + graphql: ^4.0.0-alpha.6 #path: ../graphql gql_exec: ^0.2.2 flutter: From 04e7888e5c1d4f8a61e786a7e541bfaf0116accd Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 17 Sep 2020 11:16:36 -0500 Subject: [PATCH 090/118] feat(graphql): add isMutation etc helpers to Options types --- .../graphql/lib/src/core/_base_options.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/graphql/lib/src/core/_base_options.dart b/packages/graphql/lib/src/core/_base_options.dart index c753eb16b..77c82cf97 100644 --- a/packages/graphql/lib/src/core/_base_options.dart +++ b/packages/graphql/lib/src/core/_base_options.dart @@ -7,6 +7,7 @@ import 'package:gql_exec/gql_exec.dart'; import 'package:graphql/client.dart'; import 'package:graphql/src/core/policies.dart'; +/// TODO refactor into [Request] container /// Base options. abstract class BaseOptions extends MutableDataClass { BaseOptions({ @@ -65,4 +66,21 @@ abstract class BaseOptions extends MutableDataClass { policies, context, ]; + + OperationType get type { + final definitions = + document.definitions.whereType().toList(); + if (operationName != null) { + definitions.removeWhere( + (node) => node.name.value != operationName, + ); + } + // TODO differentiate error types, add exception + assert(definitions.length == 1); + return definitions.first.type; + } + + bool get isQuery => type == OperationType.query; + bool get isMutation => type == OperationType.mutation; + bool get isSubscription => type == OperationType.subscription; } From 1cf6478262f9d1ad24d12ae4ddc9af5a85cf94e9 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 17 Sep 2020 11:17:14 -0500 Subject: [PATCH 091/118] refactor(client): clean up unused helpers, clarify shoudRebroadcast --- .../src/cache/_normalizing_data_proxy.dart | 4 ++- .../lib/src/utilities/get_from_ast.dart | 35 ------------------- 2 files changed, 3 insertions(+), 36 deletions(-) delete mode 100644 packages/graphql/lib/src/utilities/get_from_ast.dart diff --git a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart index 3e45ac50f..dc3f88092 100644 --- a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart @@ -31,7 +31,9 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { @protected bool get returnPartialData => false; - /// Flag used to request a (re)broadcast from the [QueryManager] + /// Flag used to request a (re)broadcast from the [QueryManager]. + /// + /// This is set on every [writeQuery] and [writeFragment] by default. @protected bool broadcastRequested = false; diff --git a/packages/graphql/lib/src/utilities/get_from_ast.dart b/packages/graphql/lib/src/utilities/get_from_ast.dart deleted file mode 100644 index 09d8d69ef..000000000 --- a/packages/graphql/lib/src/utilities/get_from_ast.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:gql/ast.dart'; - -List getOperationNodes(DocumentNode doc) { - if (doc.definitions == null) return []; - - return doc.definitions.whereType().toList(); -} - -String getLastOperationName(DocumentNode doc) { - final operations = getOperationNodes(doc); - - if (operations.isEmpty) return null; - - return operations.last?.name?.value; -} - -bool isOfType( - OperationType operationType, - DocumentNode doc, - String operationName, -) { - final operations = getOperationNodes(doc); - - if (operationName == null) { - if (operations.length > 1) return false; - - return operations.any( - (op) => op.type == operationType, - ); - } - - return operations.any( - (op) => op.name?.value == operationName && op.type == operationType, - ); -} From 6fc5e7e0878231ca2e3da465a16a8ef38906031e Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 17 Sep 2020 11:18:01 -0500 Subject: [PATCH 092/118] feat(client): expose store, cleanup --- packages/graphql/lib/src/cache/cache.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index 2da930d59..037070ac4 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -33,7 +33,9 @@ class GraphQLCache extends NormalizingDataProxy { store = store ?? InMemoryStore(); /// Stores the underlying normalized data. Defaults to an [InMemoryStore] - @protected + /// + /// **WARNING**: Directly editing the contents of the store will not automatically + /// rebroadcast operations. final Store store; /// `typePolicies` to pass down to [normalize] @@ -45,8 +47,10 @@ class GraphQLCache extends NormalizingDataProxy { @override final SanitizeVariables sanitizeVariables; - /// tracks the number of ongoing transactions to prevent - /// rebroadcasts until they are completed + /// Tracks the number of ongoing transactions (cache updates) + /// to prevent rebroadcasts until they are completed. + /// + /// **NOTE**: Does not track network calls @protected int inflightOptimisticTransactions = 0; @@ -56,9 +60,9 @@ class GraphQLCache extends NormalizingDataProxy { /// /// This is not meant to be called outside of the [QueryManager] bool shouldBroadcast({bool claimExecution = false}) { - if (inflightOptimisticTransactions == 0 && this.broadcastRequested) { + if (inflightOptimisticTransactions == 0 && broadcastRequested) { if (claimExecution) { - this.broadcastRequested = false; + broadcastRequested = false; } return true; } From 1e9337633d3c0b6ea61bc3d83fd7d9b1f2f20a7a Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 17 Sep 2020 11:18:51 -0500 Subject: [PATCH 093/118] fix(client): only queries are refetch safe --- .../lib/src/core/observable_query.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 632710a86..63687fafb 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -109,7 +109,10 @@ class ObservableQuery { Stream get stream => controller.stream; bool get isCurrentlyPolling => lifecycle == QueryLifecycle.polling; - bool get _isRefetchSafe { + bool get isRefetchSafe { + if (!options.isQuery) { + return false; + } switch (lifecycle) { case QueryLifecycle.completed: case QueryLifecycle.polling: @@ -127,13 +130,11 @@ class ObservableQuery { } /// Attempts to refetch, throwing error if not refetch safe - Future refetch() { - if (_isRefetchSafe) { + Future refetch() async { + if (isRefetchSafe) { return queryManager.refetchQuery(queryId); } - return Future.error( - Exception('Query is not refetch safe'), - ); + throw Exception('Query is not refetch safe'); } /// Whether it is safe to rebroadcast results due to cache @@ -264,8 +265,10 @@ class ObservableQuery { /// /// [fromRebroadcast] is used to avoid the super-edge case of infinite rebroadcasts /// (not sure if it's even possible) - void _applyCallbacks(QueryResult result, - {bool fromRebroadcast = false}) async { + void _applyCallbacks( + QueryResult result, { + bool fromRebroadcast = false, + }) async { final callbacks = [ ..._onDataCallbacks, if (!fromRebroadcast) _maybeRebroadcast From e45b240ae5992edb63e02ddd01ddc5dc9d955795 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 17 Sep 2020 11:21:18 -0500 Subject: [PATCH 094/118] feat(client): refetchSafeQueries, clarify rebroadcast calls in docs --- .../graphql/lib/src/core/query_manager.dart | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/packages/graphql/lib/src/core/query_manager.dart b/packages/graphql/lib/src/core/query_manager.dart index 09ff5f1d7..c743b1adf 100644 --- a/packages/graphql/lib/src/core/query_manager.dart +++ b/packages/graphql/lib/src/core/query_manager.dart @@ -34,6 +34,9 @@ class QueryManager { /// [ObservableQuery] registry Map queries = {}; + /// prevents rebroadcasting for some intensive bulk operation like [refetchSafeQueries] + bool rebroadcastLocked = false; + ObservableQuery watchQuery(WatchQueryOptions options) { final ObservableQuery observableQuery = ObservableQuery( queryManager: this, @@ -95,27 +98,28 @@ class QueryManager { Future query(QueryOptions options) => fetchQuery('0', options); - Future mutate(MutationOptions options) { - return fetchQuery('0', options).then((result) async { - // not sure why query id is '0', may be needs improvements - // once the mutation has been process successfully, execute callbacks - // before returning the results - final mutationCallbacks = MutationCallbackHandler( - cache: cache, - options: options, - queryId: '0', - ); + Future mutate(MutationOptions options) async { + final result = await fetchQuery('0', options); + // not sure why query id is '0', may be needs improvements + // once the mutation has been process successfully, execute callbacks + // before returning the results + final mutationCallbacks = MutationCallbackHandler( + cache: cache, + options: options, + queryId: '0', + ); - final callbacks = mutationCallbacks.callbacks; + final callbacks = mutationCallbacks.callbacks; - for (final callback in callbacks) { - await callback(result); - } + for (final callback in callbacks) { + await callback(result); + } - maybeRebroadcastQueries(); + /// [fetchQuery] attempts to broadcast from the observable, + /// but now we've called all our side effects. + maybeRebroadcastQueries(); - return result; - }); + return result; } Future fetchQuery( @@ -270,6 +274,17 @@ class QueryManager { return fetchQuery(queryId, options); } + @experimental + Future> refetchSafeQueries() async { + rebroadcastLocked = true; + final results = await Future.wait( + queries.values.where((q) => q.isRefetchSafe).map((q) => q.refetch()), + ); + rebroadcastLocked = false; + maybeRebroadcastQueries(); + return results; + } + ObservableQuery getQuery(String queryId) { if (queries.containsKey(queryId)) { return queries[queryId]; @@ -278,8 +293,9 @@ class QueryManager { return null; } - /// Add a result to the [ObservableQuery] specified by `queryId`, if it exists - /// Will [maybeRebroadcastQueries] from [addResult] if the cache has flagged the need to + /// Add a result to the [ObservableQuery] specified by `queryId`, if it exists. + /// + /// Will [maybeRebroadcastQueries] from [ObservableQuery.addResult] if the [cache] has flagged the need to. /// /// Queries are registered via [setQuery] and [watchQuery] void addQueryResult( @@ -324,16 +340,26 @@ class QueryManager { return queryResult; } + /// Rebroadcast cached queries with changed underlying data if [cache.broadcastRequested] or [force]. + /// /// Push changed data from cache to query streams. /// [exclude] is used to skip a query if it was recently executed /// (normally the query that caused the rebroadcast) /// /// Returns whether a broadcast was executed, which depends on the state of the cache. /// If there are multiple in-flight cache updates, we wait until they all complete - bool maybeRebroadcastQueries({ObservableQuery exclude}) { + /// + /// **Note on internal implementation details**: + /// There is sometimes confusion on when this is called, but rebroadcasts are requested + /// from every [addQueryResult] where `result.isNotLoading` as an [OnData] callback from [ObservableQuery]. + bool maybeRebroadcastQueries({ObservableQuery exclude, bool force = false}) { + if (rebroadcastLocked && !force) { + return false; + } + final shouldBroadast = cache.shouldBroadcast(claimExecution: true); - if (!shouldBroadast) { + if (!shouldBroadast && !force) { return false; } From ba7134aad4f755c420ebf0f600898c090df52da7 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 17 Sep 2020 11:21:52 -0500 Subject: [PATCH 095/118] feat(client): cache proxy methods on cache, resetStore with optional refetchQueries --- packages/graphql/lib/src/graphql_client.dart | 61 +++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 1c369aea5..0433c1470 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -15,7 +15,7 @@ import 'package:graphql/src/core/fetch_more.dart'; /// /// [ac]: https://www.apollographql.com/docs/react/v3.0-beta/api/core/ApolloClient/ /// [link]: https://github.com/gql-dart/gql/tree/master/links/gql_link -class GraphQLClient { +class GraphQLClient implements GraphQLDataProxy { /// Constructs a [GraphQLClient] given a [Link] and a [Cache]. GraphQLClient({ @required this.link, @@ -173,7 +173,7 @@ class GraphQLClient { /// return; /// } /// - /// print('Rew Review: ${result.data}'); + /// print('New Review: ${result.data}'); /// }); /// ``` /// {@end-tool} @@ -198,4 +198,61 @@ class GraphQLClient { previousResult: previousResult, queryManager: queryManager, ); + + /// pass through to [cache.readQuery] + readQuery(request, {optimistic}) => + cache.readQuery(request, optimistic: optimistic); + + /// pass through to [cache.readFragment] + readFragment({ + @required fragment, + @required idFields, + fragmentName, + variables, + optimistic, + }) => + cache.readFragment( + fragment: fragment, + idFields: idFields, + fragmentName: fragmentName, + variables: variables, + optimistic: optimistic, + ); + + /// pass through to [cache.writeQuery] and then rebroadcast any changes. + void writeQuery(request, {data, broadcast}) { + cache.writeQuery(request, data: data, broadcast: broadcast); + queryManager.maybeRebroadcastQueries(); + } + + /// pass through to [cache.writeFragment] and then rebroadcast any changes. + void writeFragment({ + @required fragment, + @required idFields, + @required data, + fragmentName, + variables, + broadcast, + }) { + cache.writeFragment( + fragment: fragment, + idFields: idFields, + data: data, + fragmentName: fragmentName, + variables: variables, + broadcast: broadcast, + ); + queryManager.maybeRebroadcastQueries(); + } + + /// Resets the contents of the store with [cache.store.reset()] + /// and then refetches of all queries unless [refetchQueries] is disabled + @experimental + Future> resetStore({bool refetchQueries = true}) { + cache.store.reset(); + if (refetchQueries) { + return queryManager.refetchSafeQueries(); + } + return null; + } } From 7a1a0958d39819570f5c877b9fd12090b2773e87 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 17 Sep 2020 11:35:16 -0500 Subject: [PATCH 096/118] packaging: 4.0.0-alpha.7 --- packages/graphql/CHANGELOG.md | 16 ++++++++++++++++ packages/graphql/pubspec.yaml | 2 +- packages/graphql_flutter/CHANGELOG.md | 16 ++++++++++++++++ packages/graphql_flutter/pubspec.yaml | 4 ++-- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index ab0707f82..b0a2c9fb8 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,9 +1,25 @@ +# 4.0.0-alpha.7 (2020-09-17) + +`GraphQLClient` now `implements GraphQLDataProxy`, exposing `readQuery`, `writeQuery`, `readFragment`, and `writeFragment`. The writing methods also trigger rebroadcasts, closing #728. + +It also adds an experimental `client.resetStore({refetchQueries = true})` for refetching the results of all observed queries (not mutations), and expose `cache.store` with a **WARNING** about direct access. + +* **client**: cache proxy methods on cache, resetStore with optional refetchQueries ([ba7134a](https://github.com/zino-app/graphql-flutter/commit/ba7134aad4f755c420ebf0f600898c090df52da7)) +* **client**: refetchSafeQueries, clarify rebroadcast calls in docs ([e45b240](https://github.com/zino-app/graphql-flutter/commit/e45b240ae5992edb63e02ddd01ddc5dc9d955795)) +* **client**: expose store, cleanup ([6fc5e7e](https://github.com/zino-app/graphql-flutter/commit/6fc5e7e0878231ca2e3da465a16a8ef38906031e)) +* **client**: add isMutation etc helpers to Options types ([04e7888](https://github.com/zino-app/graphql-flutter/commit/04e7888e5c1d4f8a61e786a7e541bfaf0116accd)) +* **fix**: only queries are refetch safe ([1e93376](https://github.com/zino-app/graphql-flutter/commit/1e9337633d3c0b6ea61bc3d83fd7d9b1f2f20a7a)) +* **refactor**: clean up unused helpers, clarify shoudRebroadcast ([1cf6478](https://github.com/zino-app/graphql-flutter/commit/1cf6478262f9d1ad24d12ae4ddc9af5a85cf94e9)) + + # 4.0.0-alpha.6 (2020-09-12) * **client**: fix subscription initial result from cache ([de66cff](https://github.com/zino-app/graphql-flutter/commit/de66cffab0c13f1d705bb4538f55a2e11b653a4f)) + # 4.0.0-alpha.5 (2020-09-12) * **cache**: sanitize multipart files for cache. ([4ceb800](https://github.com/zino-app/graphql-flutter/commit/4ceb8006baf4539ab423e3b3a229f194cc2eac45)) + # 4.0.0-alpha.4 (2020-09-05) * **client**: bring back old websocket link with gql adapter layer ([496d994](https://github.com/zino-app/graphql-flutter/commit/496d994e06148fbad1a394c7b3d68e43a8e8acaf)) * **client**: multipart file support from `gql_http_link==0.3.0` ([c2733ca](https://github.com/zino-app/graphql-flutter/commit/c2733ca3d33b1b50afc5b2ef7809fd1f4aa41500)) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 0b95886ac..3f7863c9b 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.6 +version: 4.0.0-alpha.7 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index caf17b6ac..7250f246b 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,9 +1,25 @@ +# 4.0.0-alpha.7 (2020-09-17) + +`GraphQLClient` now `implements GraphQLDataProxy`, exposing `readQuery`, `writeQuery`, `readFragment`, and `writeFragment`. The writing methods also trigger rebroadcasts, closing #728. + +It also adds an experimental `client.resetStore({refetchQueries = true})` for refetching the results of all observed queries (not mutations), and expose `cache.store` with a **WARNING** about direct access. + +* **client**: cache proxy methods on cache, resetStore with optional refetchQueries ([ba7134a](https://github.com/zino-app/graphql-flutter/commit/ba7134aad4f755c420ebf0f600898c090df52da7)) +* **client**: refetchSafeQueries, clarify rebroadcast calls in docs ([e45b240](https://github.com/zino-app/graphql-flutter/commit/e45b240ae5992edb63e02ddd01ddc5dc9d955795)) +* **client**: expose store, cleanup ([6fc5e7e](https://github.com/zino-app/graphql-flutter/commit/6fc5e7e0878231ca2e3da465a16a8ef38906031e)) +* **client**: add isMutation etc helpers to Options types ([04e7888](https://github.com/zino-app/graphql-flutter/commit/04e7888e5c1d4f8a61e786a7e541bfaf0116accd)) +* **fix**: only queries are refetch safe ([1e93376](https://github.com/zino-app/graphql-flutter/commit/1e9337633d3c0b6ea61bc3d83fd7d9b1f2f20a7a)) +* **refactor**: clean up unused helpers, clarify shoudRebroadcast ([1cf6478](https://github.com/zino-app/graphql-flutter/commit/1cf6478262f9d1ad24d12ae4ddc9af5a85cf94e9)) + + # 4.0.0-alpha.6 (2020-09-12) * **client**: fix subscription initial result from cache ([de66cff](https://github.com/zino-app/graphql-flutter/commit/de66cffab0c13f1d705bb4538f55a2e11b653a4f)) + # 4.0.0-alpha.5 (2020-09-12) * **cache**: sanitize multipart files for cache. ([4ceb800](https://github.com/zino-app/graphql-flutter/commit/4ceb8006baf4539ab423e3b3a229f194cc2eac45)) + # 4.0.0-alpha.4 (2020-09-05) * **client**: bring back old websocket link with gql adapter layer ([496d994](https://github.com/zino-app/graphql-flutter/commit/496d994e06148fbad1a394c7b3d68e43a8e8acaf)) * **client**: multipart file support from `gql_http_link==0.3.0` ([c2733ca](https://github.com/zino-app/graphql-flutter/commit/c2733ca3d33b1b50afc5b2ef7809fd1f4aa41500)) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 4af05e838..8aba79b12 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,10 +2,10 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.6 +version: 4.0.0-alpha.7 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.6 + graphql: ^4.0.0-alpha.7 #path: ../graphql gql_exec: ^0.2.2 flutter: From d37e81c855e0013b965613a41f1531e8b33b4292 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 11:12:37 -0500 Subject: [PATCH 097/118] chore(ci): I think fixes coverage and lint --- .circleci/config.yml | 4 +- .gitignore | 2 + .../test/cache/graphql_cache_test.dart | 1 - .../example/lib/fetchmore/main.dart | 4 +- .../graphql_flutter/example/lib/helpers.dart | 2 +- .../test/widgets/query_test.dart | 129 +++++++++++++----- 6 files changed, 101 insertions(+), 41 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 063e7618a..3e580f41d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -82,7 +82,9 @@ jobs: command: | cd packages/graphql dartfmt **/*.dart -n --set-exit-if-changed - flutter analyze + pub get + dartanalyzer lib test + cd example && pub get && dartanalyzer . - run: name: Code formating and analyzing (graphql_flutter) command: | diff --git a/.gitignore b/.gitignore index daf62a9a3..a2f735670 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,5 @@ unlinked_spec.ds !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +**/test/.test_coverage.dart diff --git a/packages/graphql/test/cache/graphql_cache_test.dart b/packages/graphql/test/cache/graphql_cache_test.dart index f840118b7..ea6e508e8 100644 --- a/packages/graphql/test/cache/graphql_cache_test.dart +++ b/packages/graphql/test/cache/graphql_cache_test.dart @@ -1,4 +1,3 @@ -import 'package:http/http.dart'; import 'package:test/test.dart'; import 'package:graphql/src/cache/cache.dart'; diff --git a/packages/graphql_flutter/example/lib/fetchmore/main.dart b/packages/graphql_flutter/example/lib/fetchmore/main.dart index c9e55f930..edc28ef73 100644 --- a/packages/graphql_flutter/example/lib/fetchmore/main.dart +++ b/packages/graphql_flutter/example/lib/fetchmore/main.dart @@ -94,7 +94,7 @@ class _MyHomePageState extends State { return Text(result.exception.toString()); } - if (result.loading && result.data == null) { + if (result.isLoading && result.data == null) { return const Center( child: CircularProgressIndicator(), ); @@ -144,7 +144,7 @@ class _MyHomePageState extends State { : const Icon(Icons.star_border), title: Text(repository['name'] as String), ), - if (result.loading) + if (result.isLoading) Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/packages/graphql_flutter/example/lib/helpers.dart b/packages/graphql_flutter/example/lib/helpers.dart index 5beeb55e6..c0c2528eb 100644 --- a/packages/graphql_flutter/example/lib/helpers.dart +++ b/packages/graphql_flutter/example/lib/helpers.dart @@ -19,7 +19,7 @@ QueryBuilder withGenericHandling(QueryBuilder builder) { return Text(result.exception.toString()); } - if (result.loading) { + if (result.isLoading) { return const Center( child: CircularProgressIndicator(), ); diff --git a/packages/graphql_flutter/test/widgets/query_test.dart b/packages/graphql_flutter/test/widgets/query_test.dart index 10e5596ed..ae789719c 100644 --- a/packages/graphql_flutter/test/widgets/query_test.dart +++ b/packages/graphql_flutter/test/widgets/query_test.dart @@ -6,10 +6,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:graphql_flutter/src/widgets/query.dart'; -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; import 'package:mockito/mockito.dart'; -class MockHttpClient extends Mock implements Client {} +class MockHttpClient extends Mock implements http.Client {} final query = gql(""" query Foo { @@ -138,10 +138,16 @@ void main() { )); verify( - mockHttpClient.post( - any, - headers: anyNamed('headers'), - body: anyNamed('body'), + mockHttpClient.send( + argThat(isA() + .having((request) => request.method, "method", "POST") + .having((request) => request.headers, "headers", isNotNull) + .having((request) => request.body, "body", isNotNull) + .having( + (request) => request.url, + "expected endpoint", + Uri.parse('https://unused/graphql'), + )), ), ).called(1); @@ -167,10 +173,16 @@ void main() { )); verify( - mockHttpClient.post( - any, - headers: anyNamed('headers'), - body: anyNamed('body'), + mockHttpClient.send( + argThat(isA() + .having((request) => request.method, "method", "POST") + .having((request) => request.headers, "headers", isNotNull) + .having((request) => request.body, "body", isNotNull) + .having( + (request) => request.url, + "expected endpoint", + Uri.parse('https://unused/graphql'), + )), ), ).called(1); @@ -195,17 +207,32 @@ void main() { )); verify( - mockHttpClient.post(any, - headers: anyNamed('headers'), body: anyNamed('body')), + mockHttpClient.send( + argThat(isA() + .having((request) => request.method, "method", "POST") + .having((request) => request.headers, "headers", isNotNull) + .having((request) => request.body, "body", isNotNull) + .having( + (request) => request.url, + "expected endpoint", + Uri.parse('https://unused/graphql'), + )), + ), ).called(1); tester.state(find.byWidget(page)).setVariables({'foo': 2}); await tester.pump(); verify( - mockHttpClient.post( - any, - headers: anyNamed('headers'), - body: anyNamed('body'), + mockHttpClient.send( + argThat(isA() + .having((request) => request.method, "method", "POST") + .having((request) => request.headers, "headers", isNotNull) + .having((request) => request.body, "body", isNotNull) + .having( + (request) => request.url, + "expected endpoint", + Uri.parse('https://unused/graphql'), + )), ), ).called(1); }); @@ -222,10 +249,16 @@ void main() { )); verify( - mockHttpClient.post( - any, - headers: anyNamed('headers'), - body: anyNamed('body'), + mockHttpClient.send( + argThat(isA() + .having((request) => request.method, "method", "POST") + .having((request) => request.headers, "headers", isNotNull) + .having((request) => request.body, "body", isNotNull) + .having( + (request) => request.url, + "expected endpoint", + Uri.parse('https://unused/graphql'), + )), ), ).called(1); @@ -234,10 +267,16 @@ void main() { .setFetchPolicy(FetchPolicy.cacheFirst); await tester.pump(); verify( - mockHttpClient.post( - any, - headers: anyNamed('headers'), - body: anyNamed('body'), + mockHttpClient.send( + argThat(isA() + .having((request) => request.method, "method", "POST") + .having((request) => request.headers, "headers", isNotNull) + .having((request) => request.body, "body", isNotNull) + .having( + (request) => request.url, + "expected endpoint", + Uri.parse('https://unused/graphql'), + )), ), ).called(1); }); @@ -254,10 +293,16 @@ void main() { )); verify( - mockHttpClient.post( - any, - headers: anyNamed('headers'), - body: anyNamed('body'), + mockHttpClient.send( + argThat(isA() + .having((request) => request.method, "method", "POST") + .having((request) => request.headers, "headers", isNotNull) + .having((request) => request.body, "body", isNotNull) + .having( + (request) => request.url, + "expected endpoint", + Uri.parse('https://unused/graphql'), + )), ), ).called(1); @@ -266,10 +311,16 @@ void main() { .setErrorPolicy(ErrorPolicy.none); await tester.pump(); verify( - mockHttpClient.post( - any, - headers: anyNamed('headers'), - body: anyNamed('body'), + mockHttpClient.send( + argThat(isA() + .having((request) => request.method, "method", "POST") + .having((request) => request.headers, "headers", isNotNull) + .having((request) => request.body, "body", isNotNull) + .having( + (request) => request.url, + "expected endpoint", + Uri.parse('https://unused/graphql'), + )), ), ).called(1); }); @@ -288,10 +339,16 @@ void main() { )); verify( - mockHttpClient.post( - any, - headers: anyNamed('headers'), - body: anyNamed('body'), + mockHttpClient.send( + argThat(isA() + .having((request) => request.method, "method", "POST") + .having((request) => request.headers, "headers", isNotNull) + .having((request) => request.body, "body", isNotNull) + .having( + (request) => request.url, + "expected endpoint", + Uri.parse('https://unused/graphql'), + )), ), ).called(1); From fe02bb8daed67ae635b97a8d4fe34fa015bbcdd8 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 12:37:31 -0500 Subject: [PATCH 098/118] refactor(client): pollInterval is now a Duration --- changelog-v3-v4.md | 7 ++-- examples/flutter_bloc/lib/repository.dart | 2 +- .../lib/src/core/observable_query.dart | 4 +- .../graphql/lib/src/core/query_options.dart | 4 +- .../websocket_link/websocket_client.dart | 37 ++++++++++--------- .../graphql/lib/src/scheduler/scheduler.dart | 11 +++--- .../example/lib/graphql_bloc/bloc.dart | 2 +- 7 files changed, 34 insertions(+), 33 deletions(-) diff --git a/changelog-v3-v4.md b/changelog-v3-v4.md index 611db87c5..75754c17a 100644 --- a/changelog-v3-v4.md +++ b/changelog-v3-v4.md @@ -7,8 +7,8 @@ v4 aims to solve a number of sore spots, particularly with caching, largely by l - There is now only a single `GraphQLCache`, which leverages [normalize](https://pub.dev/packages/normalize), Giving us a much more `apollo`ish API. - [`typePolicies`] - - [direct cache access] via `readQuery`, `writeQuery`, `readFragment`, and `writeFragment` - All of which can which can be used for [local state management] + - [direct cache access] via `readQuery`, `writeQuery`, `readFragment`, and `writeFragment` + All of which can which can be used for [local state management] - `LazyCacheMap` has been deleted - `GraphQLCache` marks itself for rebroadcasting (should fix some related issues) - **`Store`** is now a seperate concern: @@ -97,6 +97,7 @@ Subscription( ## Minor changes +- `pollInterval`, which used to be an `int` of `seconds`, is now a `Duration` - As mentioned before, `documentNode: gql(...)` is now `document: gql(...)`. - The exported `gql` utility adds `__typename` automatically. \*\*If you define your own, make sure to include `AddTypenameVisitor`, @@ -145,5 +146,5 @@ class MyQuery { ``` [local state management]: https://www.apollographql.com/docs/tutorial/local-state/#update-local-data -[`typePolicies`]: https://www.apollographql.com/docs/react/caching/cache-configuration/#the-typepolicy-type +[`typepolicies`]: https://www.apollographql.com/docs/react/caching/cache-configuration/#the-typepolicy-type [direct cache access]: https://www.apollographql.com/docs/react/caching/cache-interaction/ diff --git a/examples/flutter_bloc/lib/repository.dart b/examples/flutter_bloc/lib/repository.dart index bbc41d88f..6ffb25d88 100644 --- a/examples/flutter_bloc/lib/repository.dart +++ b/examples/flutter_bloc/lib/repository.dart @@ -20,7 +20,7 @@ class GithubRepository { variables: { 'nRepositories': numOfRepositories, }, - pollInterval: 4, + pollInterval: Duration(seconds: 4), fetchResults: true, ); diff --git a/packages/graphql/lib/src/core/observable_query.dart b/packages/graphql/lib/src/core/observable_query.dart index 63687fafb..b0e3ac03a 100644 --- a/packages/graphql/lib/src/core/observable_query.dart +++ b/packages/graphql/lib/src/core/observable_query.dart @@ -189,7 +189,7 @@ class ObservableQuery { ? QueryLifecycle.sideEffectsPending : QueryLifecycle.pending; - if (options.pollInterval != null && options.pollInterval > 0) { + if (options.pollInterval != null && options.pollInterval > Duration.zero) { startPolling(options.pollInterval); } @@ -303,7 +303,7 @@ class ObservableQuery { /// Poll the server periodically for results. /// /// Will be called by [fetchResults] automatically if [options.pollInterval] is set - void startPolling(int pollInterval) { + void startPolling(Duration pollInterval) { if (options.fetchPolicy == FetchPolicy.cacheFirst || options.fetchPolicy == FetchPolicy.cacheOnly) { throw Exception( diff --git a/packages/graphql/lib/src/core/query_options.dart b/packages/graphql/lib/src/core/query_options.dart index 7297ca9d8..085a1700f 100644 --- a/packages/graphql/lib/src/core/query_options.dart +++ b/packages/graphql/lib/src/core/query_options.dart @@ -37,7 +37,7 @@ class QueryOptions extends BaseOptions { /// The time interval (in milliseconds) on which this query should be /// re-fetched from the server. - int pollInterval; + Duration pollInterval; @override List get properties => [...super.properties, pollInterval]; @@ -88,7 +88,7 @@ class WatchQueryOptions extends QueryOptions { FetchPolicy fetchPolicy, ErrorPolicy errorPolicy, Object optimisticResult, - int pollInterval, + Duration pollInterval, this.fetchResults = false, bool eagerlyFetchResults, Context context, diff --git a/packages/graphql/lib/src/links/websocket_link/websocket_client.dart b/packages/graphql/lib/src/links/websocket_link/websocket_client.dart index ac649ac2e..9f48217a6 100644 --- a/packages/graphql/lib/src/links/websocket_link/websocket_client.dart +++ b/packages/graphql/lib/src/links/websocket_link/websocket_client.dart @@ -96,7 +96,7 @@ class SocketClientConfig { } } -enum SocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED } +enum SocketConnectionState { notConnected, connecting, connected } /// Wraps a standard web socket instance to marshal and un-marshal the server / /// client payloads into dart object representation. @@ -130,7 +130,8 @@ class SocketClient { bool _connectionWasLost = false; Timer _reconnectTimer; - WebSocket _socket; + @visibleForTesting + WebSocket socket; Stream _messageStream; @@ -152,20 +153,20 @@ class SocketClient { return; } - _connectionStateController.value = SocketConnectionState.CONNECTING; + _connectionStateController.value = SocketConnectionState.connecting; print('Connecting to websocket: $url...'); try { - _socket = await WebSocket.connect( + socket = await WebSocket.connect( url, protocols: protocols, ); - _connectionStateController.value = SocketConnectionState.CONNECTED; + _connectionStateController.value = SocketConnectionState.connected; print('Connected to websocket.'); _write(initOperation); _messageStream = - _socket.stream.map(_parseSocketMessage); + socket.stream.map(_parseSocketMessage); if (config.inactivityTimeout != null) { _keepAliveSubscription = _messagesOfType().timeout( @@ -174,9 +175,9 @@ class SocketClient { print( "Haven't received keep alive message for ${config.inactivityTimeout.inSeconds} seconds. Disconnecting.."); event.close(); - _socket.close(WebSocketStatus.goingAway); + socket.close(WebSocketStatus.goingAway); _connectionStateController.value = - SocketConnectionState.NOT_CONNECTED; + SocketConnectionState.notConnected; }, ).listen(null); } @@ -203,7 +204,7 @@ class SocketClient { } if (config.onConnectOrReconnect != null) { - config.onConnectOrReconnect(_socket); + config.onConnectOrReconnect(socket); } } catch (e) { onConnectionLost(e); @@ -227,8 +228,8 @@ class SocketClient { _subscriptionInitializers.values.forEach((s) => s.hasBeenTriggered = false); if (_connectionStateController.value != - SocketConnectionState.NOT_CONNECTED) { - _connectionStateController.value = SocketConnectionState.NOT_CONNECTED; + SocketConnectionState.notConnected) { + _connectionStateController.value = SocketConnectionState.notConnected; } if (config.autoReconnect && !_connectionStateController.isClosed) { @@ -258,7 +259,7 @@ class SocketClient { print('Disposing socket client..'); _reconnectTimer?.cancel(); await Future.wait([ - _socket?.close(), + socket?.close(), _keepAliveSubscription?.cancel(), _messageSubscription?.cancel(), _connectionStateController?.close(), @@ -293,8 +294,8 @@ class SocketClient { } void _write(final GraphQLSocketMessage message) { - if (_connectionStateController.value == SocketConnectionState.CONNECTED) { - _socket.add( + if (_connectionStateController.value == SocketConnectionState.connected) { + socket.add( json.encode( message, toEncodable: (dynamic m) => m.toJson(), @@ -325,9 +326,9 @@ class SocketClient { final Stream waitForConnectedStateWithoutTimeout = _connectionStateController .startWith( - waitForConnection ? null : SocketConnectionState.CONNECTED) + waitForConnection ? null : SocketConnectionState.connected) .where((SocketConnectionState state) => - state == SocketConnectionState.CONNECTED) + state == SocketConnectionState.connected) .take(1); final Stream waitForConnectedState = addTimeout @@ -419,8 +420,8 @@ class SocketClient { _subscriptionInitializers.remove(id); sub?.cancel(); - if (_connectionStateController.value == SocketConnectionState.CONNECTED && - _socket != null) { + if (_connectionStateController.value == SocketConnectionState.connected && + socket != null) { _write(StopOperation(id)); } }; diff --git a/packages/graphql/lib/src/scheduler/scheduler.dart b/packages/graphql/lib/src/scheduler/scheduler.dart index 918ae78f1..666c999a0 100644 --- a/packages/graphql/lib/src/scheduler/scheduler.dart +++ b/packages/graphql/lib/src/scheduler/scheduler.dart @@ -41,8 +41,7 @@ class QueryScheduler { return false; } - final Duration pollInterval = - Duration(seconds: registeredQueries[queryId].pollInterval); + final Duration pollInterval = registeredQueries[queryId].pollInterval; return registeredQueries.containsKey(queryId) && pollInterval == interval; @@ -65,13 +64,13 @@ class QueryScheduler { WatchQueryOptions options, String queryId, ) { - assert(options.pollInterval != null && options.pollInterval > 0); + assert( + options.pollInterval != null && options.pollInterval > Duration.zero, + ); registeredQueries[queryId] = options; - final Duration interval = Duration( - seconds: options.pollInterval, - ); + final interval = options.pollInterval; if (intervalQueries.containsKey(interval)) { intervalQueries[interval].add(queryId); diff --git a/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart b/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart index d2ca745c6..9d5ffc679 100644 --- a/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart +++ b/packages/graphql_flutter/example/lib/graphql_bloc/bloc.dart @@ -89,7 +89,7 @@ class Bloc { variables: { 'nRepositories': nRepositories, }, - pollInterval: 4, + pollInterval: Duration(seconds: 4), fetchResults: true, ); From 3e4870ce4b67bd9a8716af54b26061eac801c44d Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 13:50:40 -0500 Subject: [PATCH 099/118] test(client): restore old websocket client tests --- .../websocket_link/websocket_client.dart | 39 ++- packages/graphql/test/query_options_test.dart | 51 ++++ packages/graphql/test/websocket_test.dart | 264 ++++++++++++++++++ 3 files changed, 332 insertions(+), 22 deletions(-) create mode 100644 packages/graphql/test/query_options_test.dart create mode 100644 packages/graphql/test/websocket_test.dart diff --git a/packages/graphql/lib/src/links/websocket_link/websocket_client.dart b/packages/graphql/lib/src/links/websocket_link/websocket_client.dart index 9f48217a6..06beeafd0 100644 --- a/packages/graphql/lib/src/links/websocket_link/websocket_client.dart +++ b/packages/graphql/lib/src/links/websocket_link/websocket_client.dart @@ -113,6 +113,7 @@ class SocketClient { 'graphql-ws', ], this.config = const SocketClientConfig(), + this.connect = WebSocket.connect, @visibleForTesting this.randomBytesForUuid, }) { _connect(); @@ -120,13 +121,18 @@ class SocketClient { Uint8List randomBytesForUuid; final String url; - final SocketClientConfig config; final Iterable protocols; + final SocketClientConfig config; + final BehaviorSubject _connectionStateController = BehaviorSubject(); final HashMap _subscriptionInitializers = HashMap(); + + final Future Function(String url, {Iterable protocols}) + connect; + bool _connectionWasLost = false; Timer _reconnectTimer; @@ -157,10 +163,7 @@ class SocketClient { print('Connecting to websocket: $url...'); try { - socket = await WebSocket.connect( - url, - protocols: protocols, - ); + socket = await connect(url, protocols: protocols); _connectionStateController.value = SocketConnectionState.connected; print('Connected to websocket.'); _write(initOperation); @@ -365,8 +368,7 @@ class SocketClient { final Stream subscriptionComplete = addTimeout ? dataErrorComplete - .where((GraphQLSocketMessage message) => - message is SubscriptionComplete) + .where((message) => message is SubscriptionComplete) .take(1) .timeout( config.queryAndMutationTimeout, @@ -378,29 +380,22 @@ class SocketClient { }, ) : dataErrorComplete - .where((GraphQLSocketMessage message) => - message is SubscriptionComplete) + .where((message) => message is SubscriptionComplete) .take(1); subscriptionComplete.listen((_) => response.close()); dataErrorComplete - .where( - (GraphQLSocketMessage message) => message is SubscriptionData) + .where((message) => message is SubscriptionData) .cast() - .listen( - (SubscriptionData message) => response.add( - parse( - message.toJson(), - ), - ), - ); + .listen((message) => response.add( + parse(message.toJson()), + )); dataErrorComplete - .where( - (GraphQLSocketMessage message) => message is SubscriptionError) - .listen( - (GraphQLSocketMessage message) => response.addError(message)); + .where((message) => message is SubscriptionError) + .cast() + .listen((message) => response.addError(message)); if (!_subscriptionInitializers[id].hasBeenTriggered) { _write( diff --git a/packages/graphql/test/query_options_test.dart b/packages/graphql/test/query_options_test.dart new file mode 100644 index 000000000..67884b54a --- /dev/null +++ b/packages/graphql/test/query_options_test.dart @@ -0,0 +1,51 @@ +import 'package:gql/ast.dart'; +import 'package:gql/language.dart'; +import 'package:graphql/client.dart'; +import 'package:test/test.dart'; + +void main() { + group('query options', () { + group('type getters', () { + test('on QueryOptions', () { + final options = QueryOptions( + document: parseString('query { bar }'), + ); + expect(options.type, equals(OperationType.query)); + expect(options.isQuery, equals(true)); + }); + test('on MutationOptions', () { + final options = MutationOptions( + document: parseString('mutation { bar }'), + ); + expect(options.type, equals(OperationType.mutation)); + expect(options.isMutation, equals(true)); + }); + test('on SubscriptionOptions', () { + final options = SubscriptionOptions( + document: parseString('subscription { bar }'), + ); + expect(options.type, equals(OperationType.subscription)); + expect(options.isSubscription, equals(true)); + }); + }); + group('gql integration', () { + test('Options.asRequest', () { + final options = QueryOptions( + document: parseString('query { bar }'), + variables: { + 'foo': { + 'biz': 'bar', + 'bam': [1] + } + }, + context: Context.fromList([ + HttpLinkHeaders(headers: {'my': 'header'}) + ])); + final req = options.asRequest; + expect(options.document, equals(req.operation.document)); + expect(options.variables, equals(req.variables)); + expect(options.context, equals(req.context)); + }); + }); + }); +} diff --git a/packages/graphql/test/websocket_test.dart b/packages/graphql/test/websocket_test.dart new file mode 100644 index 000000000..ace556c57 --- /dev/null +++ b/packages/graphql/test/websocket_test.dart @@ -0,0 +1,264 @@ +import 'dart:async'; +import 'package:rxdart/subjects.dart'; +import 'package:test/test.dart'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:gql/language.dart'; +import 'package:graphql/client.dart'; +import 'package:graphql/src/links/websocket_link/websocket_client.dart'; +import 'package:graphql/src/links/websocket_link/websocket_messages.dart'; +import 'package:websocket/websocket.dart'; + +import './helpers.dart'; + +class EchoSocket implements WebSocket { + EchoSocket() : controller = BehaviorSubject(); + + static Future connect( + String url, { + Iterable protocols, + }) async => + EchoSocket(); + + StreamController controller; + + int closeCode; + String closeReason; + + void add(/*String|List*/ data) => controller.add(data); + + Future addStream(Stream stream) => controller.addStream(stream); + + Future close([int code, String reason]) { + closeCode ??= closeCode; + closeReason ??= closeReason; + return controller.close(); + } + + String get extensions => null; + + String get protocol => null; + + int get readyState => throw UnsupportedError('unmocked'); + void addUtf8Text(List bytes) => throw UnsupportedError('unmocked'); + + Future get done => controller.done; + + Stream get stream => controller.stream; +} + +void main() { + group('InitOperation', () { + test('null payload', () { + // ignore: deprecated_member_use_from_same_package + final operation = InitOperation(null); + expect(operation.toJson(), {'type': 'connection_init'}); + }); + test('simple payload', () { + // ignore: deprecated_member_use_from_same_package + final operation = InitOperation(42); + expect(operation.toJson(), {'type': 'connection_init', 'payload': 42}); + }); + test('complex payload', () { + // ignore: deprecated_member_use_from_same_package + final operation = InitOperation({ + 'value': 42, + 'nested': { + 'number': [3, 7], + 'string': ['foo', 'bar'] + } + }); + expect(operation.toJson(), { + 'type': 'connection_init', + 'payload': { + 'value': 42, + 'nested': { + 'number': [3, 7], + 'string': ['foo', 'bar'] + } + } + }); + }); + }); + + group('SocketClient without payload', () { + SocketClient socketClient; + final expectedMessage = r'{' + r'"type":"start","id":"01020304-0506-4708-890a-0b0c0d0e0f10",' + r'"payload":{"operationName":null,"variables":{},"query":"subscription {\n \n}"}' + r'}'; + setUp(overridePrint((log) { + socketClient = SocketClient( + 'ws://echo.websocket.org', + connect: EchoSocket.connect, + protocols: null, + config: SocketClientConfig( + delayBetweenReconnectionAttempts: Duration(milliseconds: 1), + ), + randomBytesForUuid: Uint8List.fromList( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), + ); + })); + tearDown(overridePrint((log) async { + await socketClient.dispose(); + })); + test('connection', () async { + await expectLater( + socketClient.connectionState.asBroadcastStream(), + emitsInOrder( + [ + SocketConnectionState.connecting, + SocketConnectionState.connected, + ], + ), + ); + }); + test('subscription data', () async { + final payload = Request( + operation: Operation(document: parseString('subscription {}')), + ); + final waitForConnection = true; + final subscriptionDataStream = + socketClient.subscribe(payload, waitForConnection); + await socketClient.connectionState + .where((state) => state == SocketConnectionState.connected) + .first; + + // ignore: unawaited_futures + socketClient.socket.stream + .where((message) => message == expectedMessage) + .first + .then((_) { + socketClient.socket.add(jsonEncode({ + 'type': 'data', + 'id': '01020304-0506-4708-890a-0b0c0d0e0f10', + 'payload': { + 'data': {'foo': 'bar'}, + 'errors': [ + {'message': 'error and data can coexist'} + ] + } + })); + }); + + await expectLater( + subscriptionDataStream, + emits( + // todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10' + Response( + data: {'foo': 'bar'}, + errors: [ + GraphQLError(message: 'error and data can coexist'), + ], + context: Context().withEntry(ResponseExtensions(null)), + ), + ), + ); + }); + test('resubscribe', () async { + final payload = Request( + operation: Operation(document: gql('subscription {}')), + ); + final waitForConnection = true; + final subscriptionDataStream = + socketClient.subscribe(payload, waitForConnection); + + socketClient.onConnectionLost(); + + await socketClient.connectionState + .where((state) => state == SocketConnectionState.connected) + .first; + + // ignore: unawaited_futures + socketClient.socket.stream + .where((message) => message == expectedMessage) + .first + .then((_) { + socketClient.socket.add(jsonEncode({ + 'type': 'data', + 'id': '01020304-0506-4708-890a-0b0c0d0e0f10', + 'payload': { + 'data': {'foo': 'bar'}, + 'errors': [ + {'message': 'error and data can coexist'} + ] + } + })); + }); + + await expectLater( + subscriptionDataStream, + emits( + // todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10' + Response( + data: {'foo': 'bar'}, + errors: [ + GraphQLError(message: 'error and data can coexist'), + ], + context: Context().withEntry(ResponseExtensions(null)), + ), + ), + ); + }); + }, tags: "integration"); + + group('SocketClient with const payload', () { + SocketClient socketClient; + const initPayload = {'token': 'mytoken'}; + + setUp(overridePrint((log) { + socketClient = SocketClient( + 'ws://echo.websocket.org', + connect: EchoSocket.connect, + config: SocketClientConfig(initialPayload: () => initPayload), + ); + })); + + tearDown(overridePrint((log) async { + await socketClient.dispose(); + })); + + test('connection', () async { + await socketClient.connectionState + .where((state) => state == SocketConnectionState.connected) + .first; + + await expectLater(socketClient.socket.stream.map((s) { + return jsonDecode(s)['payload']; + }), emits(initPayload)); + }); + }); + + group('SocketClient with future payload', () { + SocketClient socketClient; + const initPayload = {'token': 'mytoken'}; + + setUp(overridePrint((log) { + socketClient = SocketClient( + 'ws://echo.websocket.org', + connect: EchoSocket.connect, + config: SocketClientConfig( + initialPayload: () async { + await Future.delayed(Duration(seconds: 3)); + return initPayload; + }, + ), + ); + })); + + tearDown(overridePrint((log) async { + await socketClient.dispose(); + })); + + test('connection', () async { + await socketClient.connectionState + .where((state) => state == SocketConnectionState.connected) + .first; + + await expectLater(socketClient.socket.stream.map((s) { + return jsonDecode(s)['payload']; + }), emits(initPayload)); + }); + }); +} From 9db0b44d546a7c7193d68dba4541b7acaa255dac Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 14:12:27 -0500 Subject: [PATCH 100/118] chore: re-merge changelog and templates --- .github/ISSUE_TEMPLATE/bug_report.md | 3 + .github/ISSUE_TEMPLATE/feature_request.md | 3 + .github/ISSUE_TEMPLATE/v4-issue.md | 55 ++++++++++++++++ README.md | 10 +++ packages/graphql/CHANGELOG.md | 80 +++++++++++++++++++++++ packages/graphql/README.md | 2 +- packages/graphql_flutter/CHANGELOG.md | 80 +++++++++++++++++++++++ 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/v4-issue.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4eecf25af..ffe239242 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,9 @@ --- name: Bug report about: Create a report to help us improve +title: '' +labels: '' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 066b2d920..bbcbbe7d6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,6 +1,9 @@ --- name: Feature request about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/v4-issue.md b/.github/ISSUE_TEMPLATE/v4-issue.md new file mode 100644 index 000000000..f186dc351 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/v4-issue.md @@ -0,0 +1,55 @@ +--- +name: v4 issue +about: An issue encountered with a v4 library +title: '' +labels: v4 +assignees: micimize + +--- + +**Describe the issue** +A clear and concise description of what the problem is. +* Was there a **regression**? + + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + + +**Expected behavior** +A clear and concise description of what you expected to happen. + + +**device / execution context** +Are you on iOS, android, web, in a simulator, running from the terminal etc? This is especially important for `localhost` connection issues. + +### Other useful/optional fields + +Please fill or delete these sections if you don't fill them in + +
+ Stacktrace: + +```dart +{my stacktrace here} +``` + +
+ + +**screenshots** + + +**additional context** +What backend are you trying to use? + + +**additional notes** +Did you struggle to understand the docs or examples, or dislike the current api? + + +If you want to troubleshoot or discuss in real time, consider coming to the `support` channel in the [discord](https://discord.gg/tXTtBfC) diff --git a/README.md b/README.md index 4a4898caf..bba5b20d4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ # GraphQL Flutter +## :mega: [`v4` is now in open alpha](https://github.com/zino-app/graphql-flutter/pull/648) :mega: + + ## About this project GraphQL brings many benefits, both to the client: devices will need fewer requests, and therefore reduce data usage. And to the programmer: requests are arguable, they have the same structure as the request. @@ -35,6 +38,13 @@ Here are some examples you can follow: 1. [Starwars Example](./examples/starwars) 2. [`flutter_bloc` example](./examples/flutter_bloc) +## Articles and Videos + +External guides, tutorials, and other resources from the GraphQL Flutter community + +* [Ultimate toolchain to work with GraphQL in Flutter](https://medium.com/@v.ditsyak/ultimate-toolchain-to-work-with-graphql-in-flutter-13aef79c6484): + An intro to using `graphql_flutter` with [`artemis`](https://pub.dev/packages/artemis) for code generation and [`graphql-faker`](https://github.com/APIs-guru/graphql-faker) for API prototyping + ## Roadmap This is currently our roadmap, please feel free to request additions/changes. diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index b0a2c9fb8..1957f1a9a 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -47,6 +47,79 @@ It also adds an experimental `client.resetStore({refetchQueries = true})` for re See the [v4 changelog](../../changelog-v3-v4.md) +# [3.1.0](https://github.com/zino-app/graphql-flutter/compare/v3.0.2...v3.1.0) (2020-07-27) + + +### Bug Fixes + +* **ci:** add changelog back ([3e63c3e](https://github.com/zino-app/graphql-flutter/commit/3e63c3eddf142c99918d58fcd9a8828106327eec)) +* **ci:** add changelog back ([ac66af7](https://github.com/zino-app/graphql-flutter/commit/ac66af71f7b467adfdd8acec1db47ecba665f825)) +* **ci:** Sync master into beta ([2a0507e](https://github.com/zino-app/graphql-flutter/commit/2a0507ec3ea492ff0cc748fab80ee2258efe0b56)) +* **client:** Throw a ClientException on non-json responses, to be ([7d538e1](https://github.com/zino-app/graphql-flutter/commit/7d538e16dd626d2ff7c4f963031ff6c825f24269)), closes [#552](https://github.com/zino-app/graphql-flutter/issues/552) +* **client:** Throw a ClientException on non-json responses, to be ([060eff2](https://github.com/zino-app/graphql-flutter/commit/060eff2eec4ec701df9aabd8092755eecdc33530)), closes [#552](https://github.com/zino-app/graphql-flutter/issues/552) +* **client:** translateNetworkFailure when no route to host ([877bdb8](https://github.com/zino-app/graphql-flutter/commit/877bdb8b2e9093f58f26f5d1abf2460aa5e3e910)) +* **client:** translateNetworkFailure when no route to host ([e8b6322](https://github.com/zino-app/graphql-flutter/commit/e8b6322cc537df8cbc829be0f6182bdfdf6d0779)) +* **examples:** rebuilt & tested starwars example ([2aaffeb](https://github.com/zino-app/graphql-flutter/commit/2aaffeb835dceeb594e3cccb92cb552933609c70)) +* **examples:** rebuilt & tested starwars example ([f8e19f1](https://github.com/zino-app/graphql-flutter/commit/f8e19f1e1f6d41a68c8bd54cd4b2613be7c81f10)) +* **examples/starwars:** use git dependency for graphql_starwars_test_server ([0facc48](https://github.com/zino-app/graphql-flutter/commit/0facc4880b3cfcb6abe9f4e7ed5609b97f3fab42)) +* **examples/starwars:** use git dependency for graphql_starwars_test_server ([2fff649](https://github.com/zino-app/graphql-flutter/commit/2fff649f6a570ebb0bf4bcc0d61d50c1812044d1)) +* **flutter:** Query.didUpdateWidget and policy overrides ([6672e44](https://github.com/zino-app/graphql-flutter/commit/6672e44f1ab9fcb03a3bc046d4822c9c8aca5ef6)) +* **flutter:** Query.didUpdateWidget and policy overrides ([32f6172](https://github.com/zino-app/graphql-flutter/commit/32f617240b9a2a7ddb00e8d654384c89e6770c76)) +* **flutter:** widgets make unnecessary requests when dependencies change ([c487931](https://github.com/zino-app/graphql-flutter/commit/c487931db3a5f0b62b6c8e2387b1c630a523b627)) +* **flutter:** widgets make unnecessary requests when dependencies change ([31936ff](https://github.com/zino-app/graphql-flutter/commit/31936ff2c3cf8cc2dcf6b017868fec71320f080a)) +* **packaging:** correct dependencies, remove authors ([13f6a43](https://github.com/zino-app/graphql-flutter/commit/13f6a4356c05c6ad78e90f9b0f73579f86cf36db)) +* **packaging:** correct dependencies, remove authors ([a22d504](https://github.com/zino-app/graphql-flutter/commit/a22d5041a556cca8fa52ab59119ff8fd7ad652ec)) +* **packaging:** don't commit .flutter-plugins-dependencies ([f77dafa](https://github.com/zino-app/graphql-flutter/commit/f77dafadb2314761341b35ac250460424089e718)) +* **packaging:** don't commit .flutter-plugins-dependencies ([0857030](https://github.com/zino-app/graphql-flutter/commit/0857030d390e131d132c3d0d5984693a4462ae22)) +* **packaging:** upgrade rxdart ([20364a9](https://github.com/zino-app/graphql-flutter/commit/20364a9bbea6f2fb8f90001e7301990486b5263d)) +* **packaging:** upgrade rxdart ([1ec5f72](https://github.com/zino-app/graphql-flutter/commit/1ec5f72f63b17f2185195aa29e1402082dab77a7)) +* **release:** RunMutation return type definition ([9cb9658](https://github.com/zino-app/graphql-flutter/commit/9cb9658f745139080e435856682ea0148d814098)) +* **release:** RunMutation return type definition ([97211d4](https://github.com/zino-app/graphql-flutter/commit/97211d4ede3567693cd872b8d0222b7894aea733)) +* **tests:** don't factor tests into coverage scores ([4a9bcd4](https://github.com/zino-app/graphql-flutter/commit/4a9bcd4c708e955dbfcd432f0ce803541a343487)) +* **tests:** don't factor tests into coverage scores ([a81f780](https://github.com/zino-app/graphql-flutter/commit/a81f780efd5028b9af27b379a353c26176bc5f15)) + + +### Features + +* **examples/starwars:** add web support ([3b5bc93](https://github.com/zino-app/graphql-flutter/commit/3b5bc932042f3980180dea737cb84a45db1e846d)) +* **examples/starwars:** add web support ([f52b1db](https://github.com/zino-app/graphql-flutter/commit/f52b1dbc5bbafd0933e2b5b51b8f09c18462bd0b)) +* **graphql:** custom auth headerKey ([fc01ea5](https://github.com/zino-app/graphql-flutter/commit/fc01ea548a6e3adc47c1c927efd933b67cc396af)) +* **graphql:** custom auth headerKey ([167fac5](https://github.com/zino-app/graphql-flutter/commit/167fac5366160aa8384c3d87c900b38b065f6d59)) + +# [3.1.0-beta.7](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.6...v3.1.0-beta.7) (2020-06-04) + + +### Bug Fixes + +* **ci:** add changelog back ([3e63c3e](https://github.com/zino-app/graphql-flutter/commit/3e63c3eddf142c99918d58fcd9a8828106327eec)) +* **ci:** Sync master into beta ([2a0507e](https://github.com/zino-app/graphql-flutter/commit/2a0507ec3ea492ff0cc748fab80ee2258efe0b56)) +* **client:** FetchMoreOptions bug with operator precedence ([f8e05af](https://github.com/zino-app/graphql-flutter/commit/f8e05af52f9720eed612f13b513d25f2456a8726)) + +## [3.0.2](https://github.com/zino-app/graphql-flutter/compare/v3.0.1...v3.0.2) (2020-05-18) + + +### Bug Fixes + +* **client:** FetchMoreOptions bug with operator precedence ([f8e05af](https://github.com/zino-app/graphql-flutter/commit/f8e05af52f9720eed612f13b513d25f2456a8726)) + +# [3.1.0-beta.6](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.5...v3.1.0-beta.6) (2020-05-16) + + +### Bug Fixes + +* **packaging:** correct dependencies, remove authors ([a22d504](https://github.com/zino-app/graphql-flutter/commit/a22d5041a556cca8fa52ab59119ff8fd7ad652ec)) + +# [3.1.0-beta.5](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.4...v3.1.0-beta.5) (2020-05-10) + + +### Bug Fixes + +* **packaging:** upgrade rxdart ([20364a9](https://github.com/zino-app/graphql-flutter/commit/20364a9bbea6f2fb8f90001e7301990486b5263d)) + + +### Features + +* **graphql:** custom auth headerKey ([167fac5](https://github.com/zino-app/graphql-flutter/commit/167fac5366160aa8384c3d87c900b38b065f6d59)) # [3.1.0-beta.4](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.3...v3.1.0-beta.4) (2020-04-21) @@ -65,6 +138,13 @@ See the [v4 changelog](../../changelog-v3-v4.md) * **style:** use curly braces ([42f4da4](https://github.com/zino-app/graphql-flutter/commit/42f4da4cb5ddb9f76c34a5946eb1bf662d138cbf)) * **tests:** don't factor tests into coverage scores ([4a9bcd4](https://github.com/zino-app/graphql-flutter/commit/4a9bcd4c708e955dbfcd432f0ce803541a343487)) +## [3.0.1](https://github.com/zino-app/graphql-flutter/compare/v3.0.0...v3.0.1) (2020-04-20) + + +### Bug Fixes + +* **style:** use curly braces ([42f4da4](https://github.com/zino-app/graphql-flutter/commit/42f4da4cb5ddb9f76c34a5946eb1bf662d138cbf)) + # [3.1.0-beta.2](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.1...v3.1.0-beta.2) (2020-04-12) diff --git a/packages/graphql/README.md b/packages/graphql/README.md index 6a01c3211..cafe9dd49 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -222,7 +222,7 @@ final QueryResult result = await _client.mutate(options); if (result.hasException) { print(result.exception.toString()); - return + return; } final bool isStarred = diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index 7250f246b..56e7a6dc9 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -47,6 +47,79 @@ It also adds an experimental `client.resetStore({refetchQueries = true})` for re See the [v4 changelog](../../changelog-v3-v4.md) +# [3.1.0](https://github.com/zino-app/graphql-flutter/compare/v3.0.2...v3.1.0) (2020-07-27) + + +### Bug Fixes + +* **ci:** add changelog back ([3e63c3e](https://github.com/zino-app/graphql-flutter/commit/3e63c3eddf142c99918d58fcd9a8828106327eec)) +* **ci:** add changelog back ([ac66af7](https://github.com/zino-app/graphql-flutter/commit/ac66af71f7b467adfdd8acec1db47ecba665f825)) +* **ci:** Sync master into beta ([2a0507e](https://github.com/zino-app/graphql-flutter/commit/2a0507ec3ea492ff0cc748fab80ee2258efe0b56)) +* **client:** Throw a ClientException on non-json responses, to be ([7d538e1](https://github.com/zino-app/graphql-flutter/commit/7d538e16dd626d2ff7c4f963031ff6c825f24269)), closes [#552](https://github.com/zino-app/graphql-flutter/issues/552) +* **client:** Throw a ClientException on non-json responses, to be ([060eff2](https://github.com/zino-app/graphql-flutter/commit/060eff2eec4ec701df9aabd8092755eecdc33530)), closes [#552](https://github.com/zino-app/graphql-flutter/issues/552) +* **client:** translateNetworkFailure when no route to host ([877bdb8](https://github.com/zino-app/graphql-flutter/commit/877bdb8b2e9093f58f26f5d1abf2460aa5e3e910)) +* **client:** translateNetworkFailure when no route to host ([e8b6322](https://github.com/zino-app/graphql-flutter/commit/e8b6322cc537df8cbc829be0f6182bdfdf6d0779)) +* **examples:** rebuilt & tested starwars example ([2aaffeb](https://github.com/zino-app/graphql-flutter/commit/2aaffeb835dceeb594e3cccb92cb552933609c70)) +* **examples:** rebuilt & tested starwars example ([f8e19f1](https://github.com/zino-app/graphql-flutter/commit/f8e19f1e1f6d41a68c8bd54cd4b2613be7c81f10)) +* **examples/starwars:** use git dependency for graphql_starwars_test_server ([0facc48](https://github.com/zino-app/graphql-flutter/commit/0facc4880b3cfcb6abe9f4e7ed5609b97f3fab42)) +* **examples/starwars:** use git dependency for graphql_starwars_test_server ([2fff649](https://github.com/zino-app/graphql-flutter/commit/2fff649f6a570ebb0bf4bcc0d61d50c1812044d1)) +* **flutter:** Query.didUpdateWidget and policy overrides ([6672e44](https://github.com/zino-app/graphql-flutter/commit/6672e44f1ab9fcb03a3bc046d4822c9c8aca5ef6)) +* **flutter:** Query.didUpdateWidget and policy overrides ([32f6172](https://github.com/zino-app/graphql-flutter/commit/32f617240b9a2a7ddb00e8d654384c89e6770c76)) +* **flutter:** widgets make unnecessary requests when dependencies change ([c487931](https://github.com/zino-app/graphql-flutter/commit/c487931db3a5f0b62b6c8e2387b1c630a523b627)) +* **flutter:** widgets make unnecessary requests when dependencies change ([31936ff](https://github.com/zino-app/graphql-flutter/commit/31936ff2c3cf8cc2dcf6b017868fec71320f080a)) +* **packaging:** correct dependencies, remove authors ([13f6a43](https://github.com/zino-app/graphql-flutter/commit/13f6a4356c05c6ad78e90f9b0f73579f86cf36db)) +* **packaging:** correct dependencies, remove authors ([a22d504](https://github.com/zino-app/graphql-flutter/commit/a22d5041a556cca8fa52ab59119ff8fd7ad652ec)) +* **packaging:** don't commit .flutter-plugins-dependencies ([f77dafa](https://github.com/zino-app/graphql-flutter/commit/f77dafadb2314761341b35ac250460424089e718)) +* **packaging:** don't commit .flutter-plugins-dependencies ([0857030](https://github.com/zino-app/graphql-flutter/commit/0857030d390e131d132c3d0d5984693a4462ae22)) +* **packaging:** upgrade rxdart ([20364a9](https://github.com/zino-app/graphql-flutter/commit/20364a9bbea6f2fb8f90001e7301990486b5263d)) +* **packaging:** upgrade rxdart ([1ec5f72](https://github.com/zino-app/graphql-flutter/commit/1ec5f72f63b17f2185195aa29e1402082dab77a7)) +* **release:** RunMutation return type definition ([9cb9658](https://github.com/zino-app/graphql-flutter/commit/9cb9658f745139080e435856682ea0148d814098)) +* **release:** RunMutation return type definition ([97211d4](https://github.com/zino-app/graphql-flutter/commit/97211d4ede3567693cd872b8d0222b7894aea733)) +* **tests:** don't factor tests into coverage scores ([4a9bcd4](https://github.com/zino-app/graphql-flutter/commit/4a9bcd4c708e955dbfcd432f0ce803541a343487)) +* **tests:** don't factor tests into coverage scores ([a81f780](https://github.com/zino-app/graphql-flutter/commit/a81f780efd5028b9af27b379a353c26176bc5f15)) + + +### Features + +* **examples/starwars:** add web support ([3b5bc93](https://github.com/zino-app/graphql-flutter/commit/3b5bc932042f3980180dea737cb84a45db1e846d)) +* **examples/starwars:** add web support ([f52b1db](https://github.com/zino-app/graphql-flutter/commit/f52b1dbc5bbafd0933e2b5b51b8f09c18462bd0b)) +* **graphql:** custom auth headerKey ([fc01ea5](https://github.com/zino-app/graphql-flutter/commit/fc01ea548a6e3adc47c1c927efd933b67cc396af)) +* **graphql:** custom auth headerKey ([167fac5](https://github.com/zino-app/graphql-flutter/commit/167fac5366160aa8384c3d87c900b38b065f6d59)) + +# [3.1.0-beta.7](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.6...v3.1.0-beta.7) (2020-06-04) + + +### Bug Fixes + +* **ci:** add changelog back ([3e63c3e](https://github.com/zino-app/graphql-flutter/commit/3e63c3eddf142c99918d58fcd9a8828106327eec)) +* **ci:** Sync master into beta ([2a0507e](https://github.com/zino-app/graphql-flutter/commit/2a0507ec3ea492ff0cc748fab80ee2258efe0b56)) +* **client:** FetchMoreOptions bug with operator precedence ([f8e05af](https://github.com/zino-app/graphql-flutter/commit/f8e05af52f9720eed612f13b513d25f2456a8726)) + +## [3.0.2](https://github.com/zino-app/graphql-flutter/compare/v3.0.1...v3.0.2) (2020-05-18) + + +### Bug Fixes + +* **client:** FetchMoreOptions bug with operator precedence ([f8e05af](https://github.com/zino-app/graphql-flutter/commit/f8e05af52f9720eed612f13b513d25f2456a8726)) + +# [3.1.0-beta.6](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.5...v3.1.0-beta.6) (2020-05-16) + + +### Bug Fixes + +* **packaging:** correct dependencies, remove authors ([a22d504](https://github.com/zino-app/graphql-flutter/commit/a22d5041a556cca8fa52ab59119ff8fd7ad652ec)) + +# [3.1.0-beta.5](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.4...v3.1.0-beta.5) (2020-05-10) + + +### Bug Fixes + +* **packaging:** upgrade rxdart ([20364a9](https://github.com/zino-app/graphql-flutter/commit/20364a9bbea6f2fb8f90001e7301990486b5263d)) + + +### Features + +* **graphql:** custom auth headerKey ([167fac5](https://github.com/zino-app/graphql-flutter/commit/167fac5366160aa8384c3d87c900b38b065f6d59)) # [3.1.0-beta.4](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.3...v3.1.0-beta.4) (2020-04-21) @@ -65,6 +138,13 @@ See the [v4 changelog](../../changelog-v3-v4.md) * **style:** use curly braces ([42f4da4](https://github.com/zino-app/graphql-flutter/commit/42f4da4cb5ddb9f76c34a5946eb1bf662d138cbf)) * **tests:** don't factor tests into coverage scores ([4a9bcd4](https://github.com/zino-app/graphql-flutter/commit/4a9bcd4c708e955dbfcd432f0ce803541a343487)) +## [3.0.1](https://github.com/zino-app/graphql-flutter/compare/v3.0.0...v3.0.1) (2020-04-20) + + +### Bug Fixes + +* **style:** use curly braces ([42f4da4](https://github.com/zino-app/graphql-flutter/commit/42f4da4cb5ddb9f76c34a5946eb1bf662d138cbf)) + # [3.1.0-beta.2](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.1...v3.1.0-beta.2) (2020-04-12) From 0a7b2d1b918408f988c85fa983e7a94764391784 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 14:14:33 -0500 Subject: [PATCH 101/118] clean changelog --- packages/graphql/CHANGELOG.md | 14 -------------- packages/graphql_flutter/CHANGELOG.md | 14 -------------- 2 files changed, 28 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 1957f1a9a..ddd784fc8 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -53,38 +53,24 @@ See the [v4 changelog](../../changelog-v3-v4.md) ### Bug Fixes * **ci:** add changelog back ([3e63c3e](https://github.com/zino-app/graphql-flutter/commit/3e63c3eddf142c99918d58fcd9a8828106327eec)) -* **ci:** add changelog back ([ac66af7](https://github.com/zino-app/graphql-flutter/commit/ac66af71f7b467adfdd8acec1db47ecba665f825)) * **ci:** Sync master into beta ([2a0507e](https://github.com/zino-app/graphql-flutter/commit/2a0507ec3ea492ff0cc748fab80ee2258efe0b56)) * **client:** Throw a ClientException on non-json responses, to be ([7d538e1](https://github.com/zino-app/graphql-flutter/commit/7d538e16dd626d2ff7c4f963031ff6c825f24269)), closes [#552](https://github.com/zino-app/graphql-flutter/issues/552) -* **client:** Throw a ClientException on non-json responses, to be ([060eff2](https://github.com/zino-app/graphql-flutter/commit/060eff2eec4ec701df9aabd8092755eecdc33530)), closes [#552](https://github.com/zino-app/graphql-flutter/issues/552) * **client:** translateNetworkFailure when no route to host ([877bdb8](https://github.com/zino-app/graphql-flutter/commit/877bdb8b2e9093f58f26f5d1abf2460aa5e3e910)) -* **client:** translateNetworkFailure when no route to host ([e8b6322](https://github.com/zino-app/graphql-flutter/commit/e8b6322cc537df8cbc829be0f6182bdfdf6d0779)) * **examples:** rebuilt & tested starwars example ([2aaffeb](https://github.com/zino-app/graphql-flutter/commit/2aaffeb835dceeb594e3cccb92cb552933609c70)) -* **examples:** rebuilt & tested starwars example ([f8e19f1](https://github.com/zino-app/graphql-flutter/commit/f8e19f1e1f6d41a68c8bd54cd4b2613be7c81f10)) * **examples/starwars:** use git dependency for graphql_starwars_test_server ([0facc48](https://github.com/zino-app/graphql-flutter/commit/0facc4880b3cfcb6abe9f4e7ed5609b97f3fab42)) -* **examples/starwars:** use git dependency for graphql_starwars_test_server ([2fff649](https://github.com/zino-app/graphql-flutter/commit/2fff649f6a570ebb0bf4bcc0d61d50c1812044d1)) * **flutter:** Query.didUpdateWidget and policy overrides ([6672e44](https://github.com/zino-app/graphql-flutter/commit/6672e44f1ab9fcb03a3bc046d4822c9c8aca5ef6)) -* **flutter:** Query.didUpdateWidget and policy overrides ([32f6172](https://github.com/zino-app/graphql-flutter/commit/32f617240b9a2a7ddb00e8d654384c89e6770c76)) * **flutter:** widgets make unnecessary requests when dependencies change ([c487931](https://github.com/zino-app/graphql-flutter/commit/c487931db3a5f0b62b6c8e2387b1c630a523b627)) -* **flutter:** widgets make unnecessary requests when dependencies change ([31936ff](https://github.com/zino-app/graphql-flutter/commit/31936ff2c3cf8cc2dcf6b017868fec71320f080a)) * **packaging:** correct dependencies, remove authors ([13f6a43](https://github.com/zino-app/graphql-flutter/commit/13f6a4356c05c6ad78e90f9b0f73579f86cf36db)) -* **packaging:** correct dependencies, remove authors ([a22d504](https://github.com/zino-app/graphql-flutter/commit/a22d5041a556cca8fa52ab59119ff8fd7ad652ec)) * **packaging:** don't commit .flutter-plugins-dependencies ([f77dafa](https://github.com/zino-app/graphql-flutter/commit/f77dafadb2314761341b35ac250460424089e718)) -* **packaging:** don't commit .flutter-plugins-dependencies ([0857030](https://github.com/zino-app/graphql-flutter/commit/0857030d390e131d132c3d0d5984693a4462ae22)) * **packaging:** upgrade rxdart ([20364a9](https://github.com/zino-app/graphql-flutter/commit/20364a9bbea6f2fb8f90001e7301990486b5263d)) -* **packaging:** upgrade rxdart ([1ec5f72](https://github.com/zino-app/graphql-flutter/commit/1ec5f72f63b17f2185195aa29e1402082dab77a7)) * **release:** RunMutation return type definition ([9cb9658](https://github.com/zino-app/graphql-flutter/commit/9cb9658f745139080e435856682ea0148d814098)) -* **release:** RunMutation return type definition ([97211d4](https://github.com/zino-app/graphql-flutter/commit/97211d4ede3567693cd872b8d0222b7894aea733)) * **tests:** don't factor tests into coverage scores ([4a9bcd4](https://github.com/zino-app/graphql-flutter/commit/4a9bcd4c708e955dbfcd432f0ce803541a343487)) -* **tests:** don't factor tests into coverage scores ([a81f780](https://github.com/zino-app/graphql-flutter/commit/a81f780efd5028b9af27b379a353c26176bc5f15)) ### Features * **examples/starwars:** add web support ([3b5bc93](https://github.com/zino-app/graphql-flutter/commit/3b5bc932042f3980180dea737cb84a45db1e846d)) -* **examples/starwars:** add web support ([f52b1db](https://github.com/zino-app/graphql-flutter/commit/f52b1dbc5bbafd0933e2b5b51b8f09c18462bd0b)) * **graphql:** custom auth headerKey ([fc01ea5](https://github.com/zino-app/graphql-flutter/commit/fc01ea548a6e3adc47c1c927efd933b67cc396af)) -* **graphql:** custom auth headerKey ([167fac5](https://github.com/zino-app/graphql-flutter/commit/167fac5366160aa8384c3d87c900b38b065f6d59)) # [3.1.0-beta.7](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.6...v3.1.0-beta.7) (2020-06-04) diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index 56e7a6dc9..8d1dbdab8 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -53,38 +53,24 @@ See the [v4 changelog](../../changelog-v3-v4.md) ### Bug Fixes * **ci:** add changelog back ([3e63c3e](https://github.com/zino-app/graphql-flutter/commit/3e63c3eddf142c99918d58fcd9a8828106327eec)) -* **ci:** add changelog back ([ac66af7](https://github.com/zino-app/graphql-flutter/commit/ac66af71f7b467adfdd8acec1db47ecba665f825)) * **ci:** Sync master into beta ([2a0507e](https://github.com/zino-app/graphql-flutter/commit/2a0507ec3ea492ff0cc748fab80ee2258efe0b56)) * **client:** Throw a ClientException on non-json responses, to be ([7d538e1](https://github.com/zino-app/graphql-flutter/commit/7d538e16dd626d2ff7c4f963031ff6c825f24269)), closes [#552](https://github.com/zino-app/graphql-flutter/issues/552) -* **client:** Throw a ClientException on non-json responses, to be ([060eff2](https://github.com/zino-app/graphql-flutter/commit/060eff2eec4ec701df9aabd8092755eecdc33530)), closes [#552](https://github.com/zino-app/graphql-flutter/issues/552) * **client:** translateNetworkFailure when no route to host ([877bdb8](https://github.com/zino-app/graphql-flutter/commit/877bdb8b2e9093f58f26f5d1abf2460aa5e3e910)) -* **client:** translateNetworkFailure when no route to host ([e8b6322](https://github.com/zino-app/graphql-flutter/commit/e8b6322cc537df8cbc829be0f6182bdfdf6d0779)) * **examples:** rebuilt & tested starwars example ([2aaffeb](https://github.com/zino-app/graphql-flutter/commit/2aaffeb835dceeb594e3cccb92cb552933609c70)) -* **examples:** rebuilt & tested starwars example ([f8e19f1](https://github.com/zino-app/graphql-flutter/commit/f8e19f1e1f6d41a68c8bd54cd4b2613be7c81f10)) * **examples/starwars:** use git dependency for graphql_starwars_test_server ([0facc48](https://github.com/zino-app/graphql-flutter/commit/0facc4880b3cfcb6abe9f4e7ed5609b97f3fab42)) -* **examples/starwars:** use git dependency for graphql_starwars_test_server ([2fff649](https://github.com/zino-app/graphql-flutter/commit/2fff649f6a570ebb0bf4bcc0d61d50c1812044d1)) * **flutter:** Query.didUpdateWidget and policy overrides ([6672e44](https://github.com/zino-app/graphql-flutter/commit/6672e44f1ab9fcb03a3bc046d4822c9c8aca5ef6)) -* **flutter:** Query.didUpdateWidget and policy overrides ([32f6172](https://github.com/zino-app/graphql-flutter/commit/32f617240b9a2a7ddb00e8d654384c89e6770c76)) * **flutter:** widgets make unnecessary requests when dependencies change ([c487931](https://github.com/zino-app/graphql-flutter/commit/c487931db3a5f0b62b6c8e2387b1c630a523b627)) -* **flutter:** widgets make unnecessary requests when dependencies change ([31936ff](https://github.com/zino-app/graphql-flutter/commit/31936ff2c3cf8cc2dcf6b017868fec71320f080a)) * **packaging:** correct dependencies, remove authors ([13f6a43](https://github.com/zino-app/graphql-flutter/commit/13f6a4356c05c6ad78e90f9b0f73579f86cf36db)) -* **packaging:** correct dependencies, remove authors ([a22d504](https://github.com/zino-app/graphql-flutter/commit/a22d5041a556cca8fa52ab59119ff8fd7ad652ec)) * **packaging:** don't commit .flutter-plugins-dependencies ([f77dafa](https://github.com/zino-app/graphql-flutter/commit/f77dafadb2314761341b35ac250460424089e718)) -* **packaging:** don't commit .flutter-plugins-dependencies ([0857030](https://github.com/zino-app/graphql-flutter/commit/0857030d390e131d132c3d0d5984693a4462ae22)) * **packaging:** upgrade rxdart ([20364a9](https://github.com/zino-app/graphql-flutter/commit/20364a9bbea6f2fb8f90001e7301990486b5263d)) -* **packaging:** upgrade rxdart ([1ec5f72](https://github.com/zino-app/graphql-flutter/commit/1ec5f72f63b17f2185195aa29e1402082dab77a7)) * **release:** RunMutation return type definition ([9cb9658](https://github.com/zino-app/graphql-flutter/commit/9cb9658f745139080e435856682ea0148d814098)) -* **release:** RunMutation return type definition ([97211d4](https://github.com/zino-app/graphql-flutter/commit/97211d4ede3567693cd872b8d0222b7894aea733)) * **tests:** don't factor tests into coverage scores ([4a9bcd4](https://github.com/zino-app/graphql-flutter/commit/4a9bcd4c708e955dbfcd432f0ce803541a343487)) -* **tests:** don't factor tests into coverage scores ([a81f780](https://github.com/zino-app/graphql-flutter/commit/a81f780efd5028b9af27b379a353c26176bc5f15)) ### Features * **examples/starwars:** add web support ([3b5bc93](https://github.com/zino-app/graphql-flutter/commit/3b5bc932042f3980180dea737cb84a45db1e846d)) -* **examples/starwars:** add web support ([f52b1db](https://github.com/zino-app/graphql-flutter/commit/f52b1dbc5bbafd0933e2b5b51b8f09c18462bd0b)) * **graphql:** custom auth headerKey ([fc01ea5](https://github.com/zino-app/graphql-flutter/commit/fc01ea548a6e3adc47c1c927efd933b67cc396af)) -* **graphql:** custom auth headerKey ([167fac5](https://github.com/zino-app/graphql-flutter/commit/167fac5366160aa8384c3d87c900b38b065f6d59)) # [3.1.0-beta.7](https://github.com/zino-app/graphql-flutter/compare/v3.1.0-beta.6...v3.1.0-beta.7) (2020-06-04) From f9d1adb6545685cd4534955e27eb498ca9f9a6cb Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 14:26:14 -0500 Subject: [PATCH 102/118] docs: correct multipart example --- packages/graphql_flutter/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/graphql_flutter/README.md b/packages/graphql_flutter/README.md index c6811f72f..ed01d17aa 100644 --- a/packages/graphql_flutter/README.md +++ b/packages/graphql_flutter/README.md @@ -550,16 +550,22 @@ mutation($files: [Upload!]!) { ``` ```dart -import 'dart:io' show File; +import "package:http/http.dart" show Multipartfile; // ... -String filePath = '/aboslute/path/to/file.ext'; -final QueryResult r = await graphQLClientClient.mutate( +final myFile = MultipartFile.fromString( + "", + "just plain text", + filename: "sample_upload.txt", + contentType: MediaType("text", "plain"), +); + +final result = await graphQLClient.mutate( MutationOptions( document: gql(uploadMutation), variables: { - 'files': [File(filePath)], + 'files': [myFile], }, ) ); From ac4759a474b65b3ed2f5cc0b3779ec35e53492c3 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 14:47:55 -0500 Subject: [PATCH 103/118] examples: merge and update flutter_bloc changes --- .gitignore | 1 - .../flutter_bloc/ios/Flutter/Debug.xcconfig | 1 - .../flutter_bloc/ios/Flutter/Release.xcconfig | 1 - .../ios/Runner.xcodeproj/project.pbxproj | 73 +------------------ .../contents.xcworkspacedata | 3 - 5 files changed, 4 insertions(+), 75 deletions(-) diff --git a/.gitignore b/.gitignore index 653defa91..9f2b4602f 100644 --- a/.gitignore +++ b/.gitignore @@ -133,4 +133,3 @@ unlinked_spec.ds **/test/.test_coverage.dart -examples/flutter_bloc/ios/ diff --git a/examples/flutter_bloc/ios/Flutter/Debug.xcconfig b/examples/flutter_bloc/ios/Flutter/Debug.xcconfig index e8efba114..592ceee85 100644 --- a/examples/flutter_bloc/ios/Flutter/Debug.xcconfig +++ b/examples/flutter_bloc/ios/Flutter/Debug.xcconfig @@ -1,2 +1 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/examples/flutter_bloc/ios/Flutter/Release.xcconfig b/examples/flutter_bloc/ios/Flutter/Release.xcconfig index 399e9340e..592ceee85 100644 --- a/examples/flutter_bloc/ios/Flutter/Release.xcconfig +++ b/examples/flutter_bloc/ios/Flutter/Release.xcconfig @@ -1,2 +1 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/examples/flutter_bloc/ios/Runner.xcodeproj/project.pbxproj b/examples/flutter_bloc/ios/Runner.xcodeproj/project.pbxproj index 57a9404a9..c6f27b217 100644 --- a/examples/flutter_bloc/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/flutter_bloc/ios/Runner.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 4FCA4A18A46CBA47FB0D8F91 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 256127D6B9831B2F5D48247A /* libPods-Runner.a */; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; @@ -34,8 +33,6 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 256127D6B9831B2F5D48247A /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 29DE9A8947A6DC0B919A95D3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -48,8 +45,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B19BF5C76978D4B4DE85E74C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - E9FDA8D11F01AFD424BFE11C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -57,21 +52,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4FCA4A18A46CBA47FB0D8F91 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 24FD710717B6F73695FC2537 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 256127D6B9831B2F5D48247A /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -89,8 +75,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - DF7E2FAC9983F7DDBDE1168C /* Pods */, - 24FD710717B6F73695FC2537 /* Frameworks */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, ); sourceTree = ""; }; @@ -126,17 +111,6 @@ name = "Supporting Files"; sourceTree = ""; }; - DF7E2FAC9983F7DDBDE1168C /* Pods */ = { - isa = PBXGroup; - children = ( - 29DE9A8947A6DC0B919A95D3 /* Pods-Runner.debug.xcconfig */, - E9FDA8D11F01AFD424BFE11C /* Pods-Runner.release.xcconfig */, - B19BF5C76978D4B4DE85E74C /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -144,14 +118,12 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 96827EB112566582D5B56617 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 35B5C4075B77C584D4A3941C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -210,24 +182,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 35B5C4075B77C584D4A3941C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../Flutter/Flutter.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -242,28 +196,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 96827EB112566582D5B56617 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -315,6 +247,7 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -388,6 +321,7 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -443,6 +377,7 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/examples/flutter_bloc/ios/Runner.xcworkspace/contents.xcworkspacedata b/examples/flutter_bloc/ios/Runner.xcworkspace/contents.xcworkspacedata index 21a3cc14c..1d526a16e 100644 --- a/examples/flutter_bloc/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/examples/flutter_bloc/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - From a9eed2133c273c1d938677e4694e26f643ca9d06 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 18:51:21 -0500 Subject: [PATCH 104/118] test(client): more cache and store tests --- packages/graphql/.gitignore | 2 +- .../src/cache/_normalizing_data_proxy.dart | 1 + .../src/cache/_optimistic_transactions.dart | 2 +- packages/graphql/lib/src/cache/cache.dart | 1 + .../graphql/lib/src/cache/hive_store.dart | 26 +++--- packages/graphql/lib/src/cache/store.dart | 1 + packages/graphql/test/cache/cache_data.dart | 13 +++ .../test/cache/graphql_cache_test.dart | 62 +++++++++++++- packages/graphql/test/cache/store_test.dart | 80 +++++++++++++++++++ 9 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 packages/graphql/test/cache/store_test.dart diff --git a/packages/graphql/.gitignore b/packages/graphql/.gitignore index b12a6c0ae..82b2c106b 100644 --- a/packages/graphql/.gitignore +++ b/packages/graphql/.gitignore @@ -1,4 +1,4 @@ test/.test_coverage.dart # side effect of running test -/cache.txt +test/cache/store/test_hive_boxes/ diff --git a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart index dc3f88092..20aa036bd 100644 --- a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart @@ -35,6 +35,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { /// /// This is set on every [writeQuery] and [writeFragment] by default. @protected + @visibleForTesting bool broadcastRequested = false; /// Optional `dataIdFromObject` function to pass through to [normalize] diff --git a/packages/graphql/lib/src/cache/_optimistic_transactions.dart b/packages/graphql/lib/src/cache/_optimistic_transactions.dart index ef8b0974c..0537688d1 100644 --- a/packages/graphql/lib/src/cache/_optimistic_transactions.dart +++ b/packages/graphql/lib/src/cache/_optimistic_transactions.dart @@ -15,7 +15,7 @@ typedef CacheTransaction = GraphQLDataProxy Function(GraphQLDataProxy proxy); /// An optimistic update recorded with [GraphQLCache.recordOptimisticTransaction], /// identifiable through it's [id]. @immutable -class OptimisticPatch extends Object { +class OptimisticPatch { const OptimisticPatch(this.id, this.data); final String id; final HashMap data; diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index 037070ac4..e8f24c358 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -75,6 +75,7 @@ class GraphQLCache extends NormalizingDataProxy { /// thus data in `last` will overwrite that in `first` /// if there is a conflict @protected + @visibleForTesting List optimisticPatches = []; /// Reads dereferences an entity from the first valid optimistic layer, diff --git a/packages/graphql/lib/src/cache/hive_store.dart b/packages/graphql/lib/src/cache/hive_store.dart index a9efb53ed..dd15a1d36 100644 --- a/packages/graphql/lib/src/cache/hive_store.dart +++ b/packages/graphql/lib/src/cache/hive_store.dart @@ -15,23 +15,29 @@ class HiveStore extends Store { /// If the box is already open, the instance is returned and all provided parameters are being ignored. static final openBox = Hive.openBox; - /// Create a [HiveStore] with a [Box] with the given [boxName] (defaults to [defaultBoxName]) - /// box from [openBox(boxName)] - static Future open([ + /// Convenience factory for `HiveStore(await openBox(boxName ?? 'graphqlClientStore', path: path))` + /// + /// [boxName] defaults to [defaultBoxName], [path] is optional. + /// For full configuration of a [Box] use [HiveStore()] in tandem with [openBox] / [Hive.openBox] + static Future open({ String boxName = defaultBoxName, - ]) async => - HiveStore(await openBox(boxName)); + String path, + }) async => + HiveStore(await openBox(boxName, path: path)); - @protected + /// Direct access to the underlying [Box]. + /// + /// **WARNING**: Directly editing the contents of the store will not automatically + /// rebroadcast operations. final Box box; /// Creates a HiveStore inititalized with the given [box], defaulting to `Hive.box(defaultBoxName)` - /// + /// /// **N.B.**: [box] must already be [opened] with either [openBox], [open], or `initHiveForFlutter` from `graphql_flutter`. /// This lets us decouple the async initialization logic, making store usage elsewhere much more straightforward. - /// + /// /// [opened]: https://docs.hivedb.dev/#/README?id=open-a-box - HiveStore([ Box box ]): this.box = box ?? Hive.box(defaultBoxName); + HiveStore([Box box]) : this.box = box ?? Hive.box(defaultBoxName); @override Map get(String dataId) { @@ -58,5 +64,5 @@ class HiveStore extends Store { @override Map> toMap() => Map.unmodifiable(box.toMap()); - void reset() => box.clear(); + Future reset() => box.clear(); } diff --git a/packages/graphql/lib/src/cache/store.dart b/packages/graphql/lib/src/cache/store.dart index c10f3704c..d5c9e2476 100644 --- a/packages/graphql/lib/src/cache/store.dart +++ b/packages/graphql/lib/src/cache/store.dart @@ -36,6 +36,7 @@ class InMemoryStore extends Store { /// Normalized map that backs the store. /// Defaults to an empty [HashMap] @protected + @visibleForTesting final Map data; /// Creates an InMemoryStore inititalized with [data], diff --git a/packages/graphql/test/cache/cache_data.dart b/packages/graphql/test/cache/cache_data.dart index 26ab3ecd0..9e562b62b 100644 --- a/packages/graphql/test/cache/cache_data.dart +++ b/packages/graphql/test/cache/cache_data.dart @@ -131,6 +131,19 @@ final fileVarsTest = TestCase( }, ); +final originalCValue = { + '__typename': 'C', + 'id': 6, + 'cField': 'value', +}; +final originalCFragment = parseString(r''' +fragment partialC on C { + __typename + id + cField +} +'''); + final updatedCFragment = parseString(r''' fragment partialC on C { __typename diff --git a/packages/graphql/test/cache/graphql_cache_test.dart b/packages/graphql/test/cache/graphql_cache_test.dart index ea6e508e8..61581ad1f 100644 --- a/packages/graphql/test/cache/graphql_cache_test.dart +++ b/packages/graphql/test/cache/graphql_cache_test.dart @@ -1,3 +1,4 @@ +import 'package:graphql/src/cache/_normalizing_data_proxy.dart'; import 'package:test/test.dart'; import 'package:graphql/src/cache/cache.dart'; @@ -16,18 +17,28 @@ void main() { ); }); test('updating nested normalized fragment changes top level operation', () { + final idFields = { + '__typename': updatedCValue['__typename'], + 'id': updatedCValue['id'], + }; cache.writeFragment( fragment: updatedCFragment, - idFields: { - '__typename': updatedCValue['__typename'], - 'id': updatedCValue['id'], - }, + idFields: idFields, data: updatedCValue, ); + expect( cache.readQuery(basicTest.request), equals(updatedCBasicTestData), ); + + expect( + cache.readFragment( + fragment: updatedCFragment, + idFields: idFields, + ), + updatedCValue, + ); }); test('updating subset query only partially overrides superset query', () { @@ -69,6 +80,49 @@ void main() { '.recordOptimisticTransaction', () { final GraphQLCache cache = getTestCache(); + + test( + 'OptimisticCache.readQuery and .readFragment pass through', + () { + cache.writeQuery(basicTest.request, data: basicTest.data); + cache.broadcastRequested = false; + cache.recordOptimisticTransaction( + (proxy) { + expect( + proxy.readQuery(basicTest.request), + equals(basicTest.data), + ); + + final idFields = { + '__typename': originalCValue['__typename'], + 'id': originalCValue['id'], + }; + + expect( + proxy.readFragment( + fragment: originalCFragment, + idFields: idFields, + ), + originalCValue, + ); + + expect( + (proxy as NormalizingDataProxy).broadcastRequested, + isFalse, + ); + + return proxy; + }, + '1', + ); + + // no edits + expect(cache.broadcastRequested, isFalse); + expect(cache.optimisticPatches.first.id, equals('1')); + expect(cache.optimisticPatches.first.data, equals({})); + }, + ); + test( '.writeQuery, .readQuery(optimistic: true) round trip', () { diff --git a/packages/graphql/test/cache/store_test.dart b/packages/graphql/test/cache/store_test.dart new file mode 100644 index 000000000..5bcade1e2 --- /dev/null +++ b/packages/graphql/test/cache/store_test.dart @@ -0,0 +1,80 @@ +import 'dart:io'; + +import 'package:graphql/client.dart'; +import 'package:graphql/src/utilities/helpers.dart'; +import 'package:test/test.dart'; + +import 'package:graphql/src/cache/store.dart'; + +void main() { + group('InMemoryStore', () { + final data = { + 'id': {'key': 'value'}, + 'id2': {'otherKey': false} + }; + test('basic methods', () { + final store = InMemoryStore(); + store.put('id', data['id']); + expect(store.get('id'), equals(data['id'])); + + store.delete('id'); + expect(store.data, equals({})); + }); + test('bulk methods', () { + final store = InMemoryStore(); + + store.putAll(data); + + expect(store.data, equals(data)); + expect(store.toMap(), equals(data)); + + store.reset(); + + expect(data['id'], notNull); // no mutations + }); + }); + + group('HiveStore', () { + final data = { + 'id': {'key': 'value'}, + 'id2': {'otherKey': false} + }; + final path = './test/cache/test_hive_boxes/'; + test('basic methods', () async { + final store = + await HiveStore.open(boxName: 'basic', path: path + 'basic'); + store.put('id', data['id']); + expect(store.get('id'), equals(data['id'])); + + store.delete('id'); + expect(store.toMap(), equals({})); + + await store.box.deleteFromDisk(); + }); + test('bulk methods', () async { + final store = await HiveStore.open(boxName: 'bulk', path: path + 'bulk'); + + store.putAll(data); + expect(store.toMap(), equals(data)); + + await store.reset(); + expect(store.toMap(), equals({})); + + expect(data['id'], notNull); // no mutations + + await store.box.deleteFromDisk(); + }); + test('box rereferencing', () async { + final store = await HiveStore.open(path: path); + store.putAll(data); + + expect(HiveStore().toMap(), equals(data)); + + await store.box.deleteFromDisk(); + }); + + tearDownAll(() async { + await Directory(path).delete(recursive: true); + }); + }); +} From 5e2172e45ad70cc6ce978045c8412cc414f87005 Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 19:25:27 -0500 Subject: [PATCH 105/118] cleanup as we move towards beta --- changelog-v2-v3.md | 129 ------------------ packages/graphql/CHANGELOG.md | 14 ++ packages/graphql/pubspec.yaml | 2 +- .../graphql/test/graphql_client_test.dart | 21 ++- packages/graphql_flutter/CHANGELOG.md | 14 ++ packages/graphql_flutter/UPGRADE_GUIDE.md | 105 -------------- packages/graphql_flutter/pubspec.yaml | 4 +- 7 files changed, 41 insertions(+), 248 deletions(-) delete mode 100644 changelog-v2-v3.md delete mode 100644 packages/graphql_flutter/UPGRADE_GUIDE.md diff --git a/changelog-v2-v3.md b/changelog-v2-v3.md deleted file mode 100644 index b7b56fa24..000000000 --- a/changelog-v2-v3.md +++ /dev/null @@ -1,129 +0,0 @@ -# Migrating from v2 – v3 - -## Replace `document` with `documentNode` - -We are deprecating the `document` property from both QueryOptions and -MutationOptions, and will be completely removed from the API in the future. -Instead we encourage you to switch to `documentNode` which is [AST](https://pub.dev/packages/gql) based. - -**Before:** - -```dart -const int nRepositories = 50; - -final QueryOptions options = QueryOptions( - document: readRepositories, - variables: { - 'nRepositories': nRepositories, - }, -); -``` - -**After:** - -```dart -const int nRepositories = 50; - -final QueryOptions options = QueryOptions( - documentNode: gql(readRepositories), - variables: { - 'nRepositories': nRepositories, - }, -); -``` - -## Error Handling - exception replaces error - -Replace `results.error` with `results.exception` - -**Before:** - -```dart -final QueryResult result = await _client.query(options); - -if (result.hasError) { - print(result.error.toString()); -} -... - -``` - -**After:** - -```dart -final QueryResult result = await _client.query(options); - -if (result.hasException) { - print(result.exception.toString()); -} -... - -``` - -## Mutation Callbacks have been Moved to `MutationOptions` - -Mutation options have been moved to `MutationOptions` from the `Mutation` widget. - -**Before:** - -```dart -Mutation( - options: MutationOptions( - document: addStar, - ), - builder: ( - RunMutation runMutation, - QueryResult result, - ) { - return FloatingActionButton( - onPressed: () => runMutation({ - 'starrableId': , - }), - tooltip: 'Star', - child: Icon(Icons.star), - ); - }, - update: (Cache cache, QueryResult result) { - return cache; - }, - onCompleted: (dynamic resultData) { - print(resultData); - }, -); - -... -``` - -**After:** - -```dart - -... - -Mutation( - options: MutationOptions( - documentNode: gql(addStar), - update: (Cache cache, QueryResult result) { - return cache; - }, - onCompleted: (dynamic resultData) { - print(resultData); - }, - ), - builder: ( - RunMutation runMutation, - QueryResult result, - ) { - return FloatingActionButton( - onPressed: () => runMutation({ - 'starrableId': , - }), - tooltip: 'Star', - child: Icon(Icons.star), - ); - }, -); - -... - -``` diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index ddd784fc8..488e29c5c 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,17 @@ +# 4.0.0-alpha.8 (2020-09-24) + +This was mostly a prep release for the first v4 beta. + +* **client**: cache proxy methods on cache, resetStore with optional refetchQueries ([ba7134a](https://github.com/zino-app/graphql-flutter/commit/ba7134aad4f755c420ebf0f600898c090df52da7)) +* **test(client)**: more cache and store tests ([a9eed21](https://github.com/zino-app/graphql-flutter/commit/a9eed2133c273c1d938677e4694e26f643ca9d06)) +* **examples**: merge and update flutter_bloc changes ([ac4759a](https://github.com/zino-app/graphql-flutter/commit/ac4759a474b65b3ed2f5cc0b3779ec35e53492c3)) +* **docs**: correct multipart example ([f9d1adb](https://github.com/zino-app/graphql-flutter/commit/f9d1adb6545685cd4534955e27eb498ca9f9a6cb)) +* **chore**: re-merge changelog and templates ([9db0b44](https://github.com/zino-app/graphql-flutter/commit/9db0b44d546a7c7193d68dba4541b7acaa255dac)) +* **tests**: restore old websocket client tests (https://github.com/zino-app/graphql-flutter/commit/[3e4870c](3e4870ce4b67bd9a8716af54b26061eac801c44d)) +* **refactor**: pollInterval is now a Duration ([fe02bb8](https://github.com/zino-app/graphql-flutter/commit/fe02bb8daed67ae635b97a8d4fe34fa015bbcdd8)) +* **chore**: I think fixes coverage and lint ([d37e81c](https://github.com/zino-app/graphql-flutter/commit/d37e81c855e0013b965613a41f1531e8b33b4292)) + + # 4.0.0-alpha.7 (2020-09-17) `GraphQLClient` now `implements GraphQLDataProxy`, exposing `readQuery`, `writeQuery`, `readFragment`, and `writeFragment`. The writing methods also trigger rebroadcasts, closing #728. diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 3f7863c9b..f8aa933d1 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.7 +version: 4.0.0-alpha.8 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index d47328f85..4a55ffbe5 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -57,13 +57,13 @@ void main() { '''; MockLink link; - GraphQLClient graphQLClientClient; + GraphQLClient client; group('simple json', () { setUp(() { link = MockLink(); - graphQLClientClient = GraphQLClient( + client = GraphQLClient( cache: getTestCache(), link: link, ); @@ -115,7 +115,7 @@ void main() { ), ); - final QueryResult r = await graphQLClientClient.query(_options); + final QueryResult r = await client.query(_options); verify( link.request( @@ -153,7 +153,7 @@ void main() { (_) => Stream.fromFuture(Future.error(e)), ); - final QueryResult r = await graphQLClientClient.query( + final QueryResult r = await client.query( WatchQueryOptions( document: parseString(readRepositories), ), @@ -176,7 +176,7 @@ void main() { (_) => Stream.fromFuture(Future.error(e)), ); - final QueryResult r = await graphQLClientClient.query( + final QueryResult r = await client.query( WatchQueryOptions( document: parseString(readRepositories), ), @@ -216,7 +216,7 @@ void main() { ), ); - final ObservableQuery observable = await graphQLClientClient.watchQuery( + final ObservableQuery observable = await client.watchQuery( WatchQueryOptions( document: parseString(readSingle), eagerlyFetchResults: true, @@ -267,9 +267,8 @@ void main() { final variables = {'id': '1', 'name': 'newNameFromMutation'}; - final QueryResult response = await graphQLClientClient.mutate( - MutationOptions( - document: parseString(writeSingle), variables: variables)); + final QueryResult response = await client.mutate(MutationOptions( + document: parseString(writeSingle), variables: variables)); expect(response.data['updateSingle']['name'], variables['name']); }); @@ -297,7 +296,7 @@ void main() { ), ); - final QueryResult response = await graphQLClientClient.mutate(_options); + final QueryResult response = await client.mutate(_options); verify( link.request( @@ -345,7 +344,7 @@ void main() { (_) => Stream.fromIterable(responses), ); - final stream = graphQLClientClient.subscribe( + final stream = client.subscribe( SubscriptionOptions( document: parseString( r''' diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index 8d1dbdab8..eca3fac82 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,17 @@ +# 4.0.0-alpha.8 (2020-09-24) + +This was mostly a prep release for the first v4 beta. + +* **client**: cache proxy methods on cache, resetStore with optional refetchQueries ([ba7134a](https://github.com/zino-app/graphql-flutter/commit/ba7134aad4f755c420ebf0f600898c090df52da7)) +* **test(client)**: more cache and store tests ([a9eed21](https://github.com/zino-app/graphql-flutter/commit/a9eed2133c273c1d938677e4694e26f643ca9d06)) +* **examples**: merge and update flutter_bloc changes ([ac4759a](https://github.com/zino-app/graphql-flutter/commit/ac4759a474b65b3ed2f5cc0b3779ec35e53492c3)) +* **docs**: correct multipart example ([f9d1adb](https://github.com/zino-app/graphql-flutter/commit/f9d1adb6545685cd4534955e27eb498ca9f9a6cb)) +* **chore**: re-merge changelog and templates ([9db0b44](https://github.com/zino-app/graphql-flutter/commit/9db0b44d546a7c7193d68dba4541b7acaa255dac)) +* **tests**: restore old websocket client tests (https://github.com/zino-app/graphql-flutter/commit/[3e4870c](3e4870ce4b67bd9a8716af54b26061eac801c44d)) +* **refactor**: pollInterval is now a Duration ([fe02bb8](https://github.com/zino-app/graphql-flutter/commit/fe02bb8daed67ae635b97a8d4fe34fa015bbcdd8)) +* **chore**: I think fixes coverage and lint ([d37e81c](https://github.com/zino-app/graphql-flutter/commit/d37e81c855e0013b965613a41f1531e8b33b4292)) + + # 4.0.0-alpha.7 (2020-09-17) `GraphQLClient` now `implements GraphQLDataProxy`, exposing `readQuery`, `writeQuery`, `readFragment`, and `writeFragment`. The writing methods also trigger rebroadcasts, closing #728. diff --git a/packages/graphql_flutter/UPGRADE_GUIDE.md b/packages/graphql_flutter/UPGRADE_GUIDE.md deleted file mode 100644 index 6528632e9..000000000 --- a/packages/graphql_flutter/UPGRADE_GUIDE.md +++ /dev/null @@ -1,105 +0,0 @@ -## Upgrading from 0.x.x - -Here is a guide to fix most of the breaking changes introduced in 1.x.x. - -Some class names have been renamed: - -- Renamed `Client` to `GraphQLClient` -- Renamed `GraphqlProvider` to `GraphQLProvider` -- Renamed `GraphqlConsumer` to `GraphQLConsumer` -- Renamed `GQLError` to `GraphQLError` - -We changed the way the client handles requests, it now uses a `Link` to execute queries rather than depend on the http package. We've currently only implemented the `HttpLink`, just drop it in like so: - -```diff -void main() { -+ HttpLink link = HttpLink( -+ uri: 'https://api.github.com/graphql', -+ headers: { -+ 'Authorization': 'Bearer ', -+ }, -+ ); - -- ValueNotifier client = ValueNotifier( -+ ValueNotifier client = ValueNotifier( -- Client( -- endPoint: 'https://api.github.com/graphql', -+ GraphQLClient( - cache: GraphQLCache(), -- apiToken: '', -+ link: link, - ), - ); -} -``` - -We have made a load of changes in how queries and mutations work under the hood. To allow for these changes we had to make some small changes to the API of the `Query` and `Mutation` widgets. - -```diff -Query( -- readRepositories, -+ options: QueryOptions( -+ document: readRepositories, - variables: { - 'nRepositories': 50, - }, - pollInterval: 10, -+ ), -- builder: ({ -- bool loading, -- var data, -- String error, -- }) { -+ builder: (QueryResult result, { VoidCallback refetch }) { -- if (error != '') { -- return Text(error); -+ if (result.errors != null) { -+ return Text(result.errors.toString()); - } - -- if (loading) { -+ if (result.loading) { - return Text('Loading'); - } - -- List repositories = data['viewer']['repositories']['nodes']; -+ List repositories = result.data['viewer']['repositories']['nodes']; - - return ListView.builder( - itemCount: repositories.length, - itemBuilder: (context, index) { - final repository = repositories[index]; - - return Text(repository['name']); - }); - }, -); -``` - -```diff -Mutation( -- addStar, -+ options: MutationOptions( -+ document: addStar, -+ ), - builder: ( -- runMutation, { -- bool loading, -- var data, -- String error, -+ RunMutation runMutation, -+ QueryResult result, -- }) { -+ ) { - return FloatingActionButton( - onPressed: () => runMutation({ - 'starrableId': , - }), - tooltip: 'Star', - child: Icon(Icons.star), - ); - }, -); -``` - -That's it! You should now be able to use the latest version of our library. diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 8aba79b12..a225c3ea0 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,10 +2,10 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.7 +version: 4.0.0-alpha.8 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.7 + graphql: ^4.0.0-alpha.8 #path: ../graphql gql_exec: ^0.2.2 flutter: From 8e578077b6d70692fc06fc8ca588b5749a00087a Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 19:41:49 -0500 Subject: [PATCH 106/118] fix changelog mangle --- packages/graphql/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 488e29c5c..19b81ad2b 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -7,7 +7,7 @@ This was mostly a prep release for the first v4 beta. * **examples**: merge and update flutter_bloc changes ([ac4759a](https://github.com/zino-app/graphql-flutter/commit/ac4759a474b65b3ed2f5cc0b3779ec35e53492c3)) * **docs**: correct multipart example ([f9d1adb](https://github.com/zino-app/graphql-flutter/commit/f9d1adb6545685cd4534955e27eb498ca9f9a6cb)) * **chore**: re-merge changelog and templates ([9db0b44](https://github.com/zino-app/graphql-flutter/commit/9db0b44d546a7c7193d68dba4541b7acaa255dac)) -* **tests**: restore old websocket client tests (https://github.com/zino-app/graphql-flutter/commit/[3e4870c](3e4870ce4b67bd9a8716af54b26061eac801c44d)) +* **tests**: restore old websocket client tests ([3e4870c](https://github.com/zino-app/graphql-flutter/commit/3e4870ce4b67bd9a8716af54b26061eac801c44d)) * **refactor**: pollInterval is now a Duration ([fe02bb8](https://github.com/zino-app/graphql-flutter/commit/fe02bb8daed67ae635b97a8d4fe34fa015bbcdd8)) * **chore**: I think fixes coverage and lint ([d37e81c](https://github.com/zino-app/graphql-flutter/commit/d37e81c855e0013b965613a41f1531e8b33b4292)) From 3e06854f2937b112de8552a6dc5d3cb2397c4dac Mon Sep 17 00:00:00 2001 From: micimize Date: Thu, 24 Sep 2020 20:18:23 -0500 Subject: [PATCH 107/118] docs: update graphql readme --- packages/graphql/README.md | 167 ++++++++++++++++--------------------- 1 file changed, 73 insertions(+), 94 deletions(-) diff --git a/packages/graphql/README.md b/packages/graphql/README.md index cafe9dd49..8a2646b3c 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -18,7 +18,7 @@ First, depend on this package: ```yaml dependencies: - graphql: ^4.0.0-alpha + graphql: ^4.0.0-beta ``` And then import it inside your dart code: @@ -31,16 +31,6 @@ import 'package:graphql/client.dart'; Find the migration from version 3 to version 4 [here](./../../changelog-v3-v4.md). -### Parsing at build-time - -To parse documents at build-time use `ast_builder` from -[`package:gql_code_gen`](https://pub.dev/packages/gql_code_gen): - -```yaml -dev_dependencies: - gql_code_gen: ^0.1.0 -``` - ## Usage To connect to a GraphQL Server, we first need to create a `GraphQLClient`. A `GraphQLClient` requires both a `cache` and a `link` to be initialized. @@ -51,11 +41,11 @@ In our example below, we will be using the Github Public API. we are going to us // ... final HttpLink _httpLink = HttpLink( - 'https://api.github.com/graphql', + 'https://api.github.com/graphql', ); final AuthLink _authLink = AuthLink( - getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', + getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', ); final Link _link = _authLink.concat(_httpLink); @@ -72,57 +62,6 @@ final GraphQLClient _client = GraphQLClient( ``` -### Combining Multiple Links - -`graphql` and `graphql_flutter` now use the [`gql_link`] system, which allows for any kind of routing you might need: -![diagram](https://github.com/gql-dart/gql/blob/master/links/gql_link/assets/gql_link.svg) - -A quick rundown of the composition api: - -```dart -Link.from([ - // common links run before every request - AuthLink(getToken: commonAuthenticator), - DedupeLink(), // dedupe requests - ErrorLink(onException: reportClientException), -]).split( // split terminating links, or they will break - (request) => request.isSubscription, - MyCustomSubscriptionAuthLink().concat( - WebsocketLink(mySubscriptionEndpoint), - ), - HttpLink(myAppEndpoint), -); -// adding links after here would be pointless, as they would never be accessed -``` - -When combining links, it is important to note that: - -- Terminating links like `HttpLink` and `WebsocketLink` must come at the end of a route, and will not call links following them. -- Link order is very important. In `HttpLink(myEndpoint).concat(AuthLink(getToken: authenticate))`, the `AuthLink` will never be called. - -#### Using Concat - -```dart -final Link _link = _authLink.concat(_httpLink); -``` - -#### Using Links.from - -`Link.from` joins multiple links into a single link at once. - -```dart -final Link _link = Link.from([_authLink, _httpLink]); -``` - -#### Using Links.split - -`Link.split` routes the request based on some condition. -**NB**: `WebSocketLink` and other "terminating links" must be used with `split` when there are multiple. - -```dart -link = Link.split((request) => request.isSubscription, websocketLink, link); -``` - Once you have initialized a client, you can run queries and mutations. ### Query @@ -236,27 +175,77 @@ if (isStarred) { // ... ``` -### AST documents +## Links -> We are deprecating `document` and recommend you update your application to use -> `document` instead. `document` will be removed from the api in a future version. +`graphql` and `graphql_flutter` now use the [`gql_link`] system, re-exporting: +* [gql_http_link](https://pub.dev/packages/gql_http_link) +* [gql_error_link](https://pub.dev/packages/gql_error_link) +* [gql_dedupe_link](https://pub.dev/packages/gql_dedupe_link) +* [gql_link](https://pub.dev/packages/gql_link) -For example: +As well as our own custom `WebSocketLink` and `AuthLink` -```dart -// ... +### Composing Links +The [`gql_link`] systm has a well-specified routing system: +![diagram](https://github.com/gql-dart/gql/blob/master/links/gql_link/assets/gql_link.svg) -final MutationOptions options = MutationOptions( - document: gql(addStar), - variables: { - 'starrableId': repositoryID, - }, +A quick rundown of the composition api: + +```dart +Link.from([ + // common links run before every request + AuthLink(getToken: commonAuthenticator), + DedupeLink(), // dedupe requests + ErrorLink(onException: reportClientException), +]).split( // split terminating links, or they will break + (request) => request.isSubscription, + MyCustomSubscriptionAuthLink().concat( + WebsocketLink(mySubscriptionEndpoint), + ), + HttpLink(myAppEndpoint), ); +// adding links after here would be pointless, as they would never be accessed +``` -// ... +When combining links, **it isimportant to note that**: + +- Terminating links like `HttpLink` and `WebsocketLink` must come at the end of a route, and will not call links following them. +- Link order is very important. In `HttpLink(myEndpoint).concat(AuthLink(getToken: authenticate))`, the `AuthLink` will never be called. + +#### Using Concat + +```dart +final Link _link = _authLink.concat(_httpLink); ``` -With [`package:gql_code_gen`](https://pub.dev/packages/gql_code_gen) you can parse your `*.graphql` files at build-time. +#### Using Links.from + +`Link.from` joins multiple links into a single link at once. + +```dart +final Link _link = Link.from([_authLink, _httpLink]); +``` + +#### Using Links.split + +`Link.split` routes the request based on some condition. +**NB**: `WebSocketLink` and other "terminating links" must be used with `split` when there are multiple. + +```dart +link = Link.split((request) => request.isSubscription, websocketLink, link); +``` + +## Parsing ASTs at build-time + +All `document` arguments are `DocumentNode`s from `gql/ast`. +We supply a `gql` helper for parsing, them, but you can also +parse documents at build-time use `ast_builder` from +[`package:gql_code_gen`](https://pub.dev/packages/gql_code_gen): + +```yaml +dev_dependencies: + gql_code_gen: ^0.1.5 +``` **`add_star.graphql`**: @@ -285,27 +274,14 @@ final MutationOptions options = MutationOptions( // ... ``` -## Links - -### `ErrorLink` -Perform custom logic when a GraphQL or network error happens, such as logging or -signing out. - -```dart -final ErrorLink errorLink = ErrorLink(errorHandler: (ErrorResponse response) { - Operation operation = response.operation; - FetchResult result = response.fetchResult; - OperationException exception = response.exception; - print(exception.toString()); -}); -``` +### `PersistedQueriesLink` (experimental) :warning: OUT OF SERVICE :warning: -### `PersistedQueriesLink` (experimental) +**NOTE**: There is [a PR](https://github.com/zino-app/graphql-flutter/pull/699) for migrating the `v3` `PersistedQueriesLink`, and it works, but requires more consideration. It will be fixed before `v4` `stable` is published -To improve performance you can make use of a concept introduced by [Apollo](https://www.apollographql.com/) called [Automatic persisted queries](https://www.apollographql.com/docs/apollo-server/performance/apq/) (or short "APQ") to send smaller requests and even enabled CDN caching for your GraphQL API. +To improve performance you can make use of a concept introduced by [apollo] called [Automatic persisted queries] (or short "APQ") to send smaller requests and even enabled CDN caching for your GraphQL API. -**ATTENTION:** This also requires you to have a GraphQL server that supports APQ, like [Apollo's GraphQL Server](https://www.apollographql.com/docs/apollo-server/) and will only work for queries (but not for mutations or subscriptions). +**ATTENTION:** This also requires you to have a GraphQL server that supports APQ, like [Apollo's GraphQL Server] and will only work for queries (but not for mutations or subscriptions). You can than use it simply by prepending a `PersistedQueriesLink` to your normal `HttpLink`: @@ -339,3 +315,6 @@ final Link _link = _apqLink.concat(_httpLink); [discord-badge]: https://img.shields.io/discord/559455668810153989.svg?style=flat-square&logo=discord&logoColor=ffffff [discord-link]: https://discord.gg/tXTtBfC [`gql_link`]: https://github.com/gql-dart/gql/tree/master/links/gql_link +[apollo]: https://www.apollographql.com/ +[Automatic persisted queries]: https://www.apollographql.com/docs/apollo-server/performance/apq/ +[Apollo's GraphQL Server]: https://www.apollographql.com/docs/apollo-server/ From 2f04058b0dd2d739cd423ccea616c4574f9cf9eb Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 25 Sep 2020 14:53:42 -0500 Subject: [PATCH 108/118] refactor(client): Fragment and FragmentRequest for more normalized api --- .../src/cache/_normalizing_data_proxy.dart | 42 ++++--- packages/graphql/lib/src/cache/cache.dart | 10 +- .../graphql/lib/src/cache/data_proxy.dart | 24 ++-- packages/graphql/lib/src/cache/fragment.dart | 113 ++++++++++++++++++ packages/graphql/lib/src/graphql_client.dart | 28 ++--- packages/graphql/test/cache/cache_data.dart | 39 +++--- .../test/cache/graphql_cache_test.dart | 20 ++-- 7 files changed, 195 insertions(+), 81 deletions(-) create mode 100644 packages/graphql/lib/src/cache/fragment.dart diff --git a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart index 20aa036bd..295b31ea6 100644 --- a/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart +++ b/packages/graphql/lib/src/cache/_normalizing_data_proxy.dart @@ -1,8 +1,7 @@ +import 'package:graphql/src/cache/fragment.dart'; import "package:meta/meta.dart"; import 'package:gql_exec/gql_exec.dart' show Request; -import 'package:gql/ast.dart' show DocumentNode; - import 'package:normalize/normalize.dart'; import './data_proxy.dart'; @@ -75,23 +74,22 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { returnPartialData: returnPartialData, ); - Map readFragment({ - @required DocumentNode fragment, - @required Map idFields, - String fragmentName, - Map variables, + Map readFragment( + FragmentRequest fragmentRequest, { bool optimistic = true, }) => denormalizeFragment( + // provided from cache reader: (dataId) => readNormalized(dataId, optimistic: optimistic), - fragment: fragment, - idFields: idFields, - fragmentName: fragmentName, - variables: sanitizeVariables(variables), typePolicies: typePolicies, - addTypename: addTypename ?? false, dataIdFromObject: dataIdFromObject, returnPartialData: returnPartialData, + addTypename: addTypename ?? false, + // provided from request + fragment: fragmentRequest.fragment.document, + idFields: fragmentRequest.idFields, + fragmentName: fragmentRequest.fragment.fragmentName, + variables: sanitizeVariables(fragmentRequest.variables), ); void writeQuery( @@ -113,23 +111,23 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy { } } - void writeFragment({ - @required DocumentNode fragment, - @required Map idFields, + void writeFragment( + FragmentRequest request, { @required Map data, - String fragmentName, - Map variables, bool broadcast = true, }) { normalizeFragment( + // provided from cache writer: (dataId, value) => writeNormalized(dataId, value), - fragment: fragment, - idFields: idFields, - data: data, - fragmentName: fragmentName, - variables: sanitizeVariables(variables), typePolicies: typePolicies, dataIdFromObject: dataIdFromObject, + // provided from request + fragment: request.fragment.document, + idFields: request.idFields, + fragmentName: request.fragment.fragmentName, + variables: sanitizeVariables(request.variables), + // data + data: data, ); if (broadcast ?? true) { broadcastRequested = true; diff --git a/packages/graphql/lib/src/cache/cache.dart b/packages/graphql/lib/src/cache/cache.dart index e8f24c358..c98fc9329 100644 --- a/packages/graphql/lib/src/cache/cache.dart +++ b/packages/graphql/lib/src/cache/cache.dart @@ -10,14 +10,22 @@ import 'package:normalize/normalize.dart'; export 'package:graphql/src/cache/data_proxy.dart'; export 'package:graphql/src/cache/store.dart'; export 'package:graphql/src/cache/hive_store.dart'; +export 'package:graphql/src/cache/fragment.dart'; typedef VariableEncoder = Object Function(Object t); -/// Optimmistic GraphQL Entity cache with [normalize] [TypePolicy] support +/// Optimistic GraphQL Entity cache with [normalize] [TypePolicy] support /// and configurable [store]. /// /// **NOTE**: The default [InMemoryStore] does _not_ persist to disk. /// The recommended store for persistent environments is the [HiveStore]. +/// +/// [dataIdFromObject] and [typePolicies] are passed down to [normalize] operations, which say: +/// > IDs are determined by the following: +/// > +/// > 1. If a `TypePolicy` is provided for the given type, it's `TypePolicy.keyFields` are used. +/// > 2. If a `dataIdFromObject` funciton is provided, the result is used. +/// > 3. The `id` or `_id` field (respectively) are used. class GraphQLCache extends NormalizingDataProxy { GraphQLCache({ Store store, diff --git a/packages/graphql/lib/src/cache/data_proxy.dart b/packages/graphql/lib/src/cache/data_proxy.dart index d24a74489..255656f46 100644 --- a/packages/graphql/lib/src/cache/data_proxy.dart +++ b/packages/graphql/lib/src/cache/data_proxy.dart @@ -3,6 +3,8 @@ import "package:meta/meta.dart"; import 'package:gql_exec/gql_exec.dart' show Request; import 'package:gql/ast.dart' show DocumentNode; +import './fragment.dart'; + /// A proxy to the normalized data living in our store. /// /// This interface allows a user to read and write @@ -16,12 +18,10 @@ abstract class GraphQLDataProxy { /// Reads a GraphQL fragment from any arbitrary id. /// /// If there is more than one fragment in the provided document - /// then a `fragmentName` must be provided to select the correct fragment. - Map readFragment({ - @required DocumentNode fragment, - @required Map idFields, - String fragmentName, - Map variables, + /// then a `fragmentName` must be provided to `fragmentRequest.fragment` + /// to select the correct fragment. + Map readFragment( + FragmentRequest fragmentRequest, { bool optimistic, }); @@ -42,13 +42,11 @@ abstract class GraphQLDataProxy { /// then [broadcast] changes to watchers unless `broadcast: false` /// /// If there is more than one fragment in the provided document - /// then a `fragmentName` must be provided to select the correct fragment. - void writeFragment({ - @required DocumentNode fragment, - @required Map idFields, - @required Map data, - String fragmentName, - Map variables, + /// then a `fragmentName` must be provided to `fragmentRequest.fragment` + /// to select the correct fragment. + void writeFragment( + FragmentRequest fragmentRequest, { + Map data, bool broadcast, }); } diff --git a/packages/graphql/lib/src/cache/fragment.dart b/packages/graphql/lib/src/cache/fragment.dart new file mode 100644 index 000000000..c6ce9de7b --- /dev/null +++ b/packages/graphql/lib/src/cache/fragment.dart @@ -0,0 +1,113 @@ +import "package:collection/collection.dart"; +import "package:gql/ast.dart"; +import 'package:graphql/client.dart'; +import "package:meta/meta.dart"; + +/// A fragment in a [document], optionally defined by [fragmentName] +@immutable +class Fragment { + /// Document containing at least one [FragmentDefinitionNode] + final DocumentNode document; + + /// Name of the fragment definition + /// + /// Must be specified if [document] contains more than one [FragmentDefinitionNode] + final String fragmentName; + + const Fragment({ + @required this.document, + this.fragmentName, + }) : assert(document != null); + + List _getChildren() => [ + document, + fragmentName, + ]; + + @override + bool operator ==(Object o) => + identical(this, o) || + (o is Fragment && + const ListEquality( + DeepCollectionEquality(), + ).equals( + o._getChildren(), + _getChildren(), + )); + + @override + int get hashCode => const ListEquality( + DeepCollectionEquality(), + ).hash( + _getChildren(), + ); + + @override + String toString() => + "Fragment(document: $document, fragmentName: $fragmentName)"; + + /// helper for building a [FragmentRequest] + @experimental + FragmentRequest asRequest({ + @required Map idFields, + Map variables = const {}, + }) => + FragmentRequest(fragment: this, idFields: idFields, variables: variables); +} + +/// Cache access request of [fragment] with [variables]. +@immutable +class FragmentRequest { + /// [Fragment] to be read or written + final Fragment fragment; + + /// Variables of the fragment for this request + final Map variables; + + /// Map which includes all identifying data (usually `{__typename, id }`) + final Map idFields; + + const FragmentRequest({ + @required this.fragment, + @required this.idFields, + this.variables = const {}, + }) : assert(fragment != null), + assert(idFields != null); + + List _getChildren() => [ + fragment, + variables, + idFields, + ]; + + @override + bool operator ==(Object o) => + identical(this, o) || + (o is FragmentRequest && + const ListEquality( + DeepCollectionEquality(), + ).equals( + o._getChildren(), + _getChildren(), + )); + + @override + int get hashCode => const ListEquality( + DeepCollectionEquality(), + ).hash( + _getChildren(), + ); + + @override + String toString() => + "FragmentRequest(fragment: $fragment, variables: $variables)"; +} + +extension OperationRequestHelper on Operation { + /// helper for building a [Request] + @experimental + Request asRequest({ + Map variables = const {}, + }) => + Request(operation: this, variables: variables); +} diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 0433c1470..8387db5c8 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -204,18 +204,12 @@ class GraphQLClient implements GraphQLDataProxy { cache.readQuery(request, optimistic: optimistic); /// pass through to [cache.readFragment] - readFragment({ - @required fragment, - @required idFields, - fragmentName, - variables, + readFragment( + fragmentRequest, { optimistic, }) => cache.readFragment( - fragment: fragment, - idFields: idFields, - fragmentName: fragmentName, - variables: variables, + fragmentRequest, optimistic: optimistic, ); @@ -226,21 +220,15 @@ class GraphQLClient implements GraphQLDataProxy { } /// pass through to [cache.writeFragment] and then rebroadcast any changes. - void writeFragment({ - @required fragment, - @required idFields, - @required data, - fragmentName, - variables, + void writeFragment( + fragmentRequest, { broadcast, + data, }) { cache.writeFragment( - fragment: fragment, - idFields: idFields, - data: data, - fragmentName: fragmentName, - variables: variables, + fragmentRequest, broadcast: broadcast, + data: data, ); queryManager.maybeRebroadcastQueries(); } diff --git a/packages/graphql/test/cache/cache_data.dart b/packages/graphql/test/cache/cache_data.dart index 9e562b62b..7c77e16b6 100644 --- a/packages/graphql/test/cache/cache_data.dart +++ b/packages/graphql/test/cache/cache_data.dart @@ -1,5 +1,6 @@ import 'package:gql_exec/gql_exec.dart'; import 'package:gql/language.dart'; +import 'package:graphql/client.dart' show Fragment; import 'package:graphql/src/utilities/helpers.dart'; import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; @@ -136,22 +137,30 @@ final originalCValue = { 'id': 6, 'cField': 'value', }; -final originalCFragment = parseString(r''' -fragment partialC on C { - __typename - id - cField -} -'''); +final originalCFragment = Fragment( + document: parseString( + r''' + fragment partialC on C { + __typename + id + cField + } + ''', + ), +); -final updatedCFragment = parseString(r''' -fragment partialC on C { - __typename - id - new - cField -} -'''); +final updatedCFragment = Fragment( + document: parseString( + r''' + fragment partialC on C { + __typename + id + new + cField + } + ''', + ), +); final updatedCValue = { '__typename': 'C', diff --git a/packages/graphql/test/cache/graphql_cache_test.dart b/packages/graphql/test/cache/graphql_cache_test.dart index 61581ad1f..6d1722628 100644 --- a/packages/graphql/test/cache/graphql_cache_test.dart +++ b/packages/graphql/test/cache/graphql_cache_test.dart @@ -22,8 +22,9 @@ void main() { 'id': updatedCValue['id'], }; cache.writeFragment( - fragment: updatedCFragment, - idFields: idFields, + updatedCFragment.asRequest( + idFields: idFields, + ), data: updatedCValue, ); @@ -34,8 +35,9 @@ void main() { expect( cache.readFragment( - fragment: updatedCFragment, - idFields: idFields, + updatedCFragment.asRequest( + idFields: idFields, + ), ), updatedCValue, ); @@ -99,10 +101,9 @@ void main() { }; expect( - proxy.readFragment( - fragment: originalCFragment, + proxy.readFragment(originalCFragment.asRequest( idFields: idFields, - ), + )), originalCValue, ); @@ -147,11 +148,10 @@ void main() { cache.recordOptimisticTransaction( (proxy) => proxy ..writeFragment( - fragment: updatedCFragment, - idFields: { + updatedCFragment.asRequest(idFields: { '__typename': updatedCValue['__typename'], 'id': updatedCValue['id'], - }, + }), data: updatedCValue, ), '2', From 00f4a971fa4b1aa14b568b16b25b31b98ef70a4b Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 25 Sep 2020 16:27:11 -0500 Subject: [PATCH 109/118] docs: update docs, add more sections --- README.md | 7 +- packages/graphql/README.md | 349 +++++++++++++--- .../graphql/lib/src/cache/data_proxy.dart | 90 +++- packages/graphql/lib/src/core/policies.dart | 1 - .../lib/src/exceptions/exceptions_next.dart | 2 + packages/graphql/lib/src/graphql_client.dart | 8 +- packages/graphql/pubspec.yaml | 2 +- .../graphql/test/graphql_client_test.dart | 95 +++++ packages/graphql_flutter/README.md | 388 ++++++------------ .../example/lib/graphql_widget/main.dart | 8 +- 10 files changed, 625 insertions(+), 325 deletions(-) diff --git a/README.md b/README.md index bba5b20d4..815963d15 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ ## :mega: [`v4` is now in open alpha](https://github.com/zino-app/graphql-flutter/pull/648) :mega: - ## About this project GraphQL brings many benefits, both to the client: devices will need fewer requests, and therefore reduce data usage. And to the programmer: requests are arguable, they have the same structure as the request. @@ -40,9 +39,9 @@ Here are some examples you can follow: ## Articles and Videos -External guides, tutorials, and other resources from the GraphQL Flutter community +External guides, tutorials, and other resources from the GraphQL Flutter community -* [Ultimate toolchain to work with GraphQL in Flutter](https://medium.com/@v.ditsyak/ultimate-toolchain-to-work-with-graphql-in-flutter-13aef79c6484): +- [Ultimate toolchain to work with GraphQL in Flutter](https://medium.com/@v.ditsyak/ultimate-toolchain-to-work-with-graphql-in-flutter-13aef79c6484): An intro to using `graphql_flutter` with [`artemis`](https://pub.dev/packages/artemis) for code generation and [`graphql-faker`](https://github.com/APIs-guru/graphql-faker) for API prototyping ## Roadmap @@ -61,7 +60,7 @@ This is currently our roadmap, please feel free to request additions/changes. | Optimistic results | ✅ | | Modularity | ✅ | | Automatic Persisted Queries | ✅ | -| Client state management | 🔜 | +| Client state management | ✅ | ## Contributing diff --git a/packages/graphql/README.md b/packages/graphql/README.md index 8a2646b3c..6750fb36f 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -12,6 +12,38 @@ # GraphQL Client +[`graphql/client.dart`](https://pub.dev/packages/graphql) is a GraphQL client for dart modeled on the [apollo client], and is currently the most popular GraphQL client for dart. It is co-developed alongside [`graphql_flutter`](https://pub.dev/packages/graphql_flutter) [on github](https://github.com/zino-app/graphql-flutter), where you can find more in-depth examples. We also have a lively community alongside the rest of the GraphQL Dart community on [discord][discord-link]. + +As of `v4`, it is built on foundational libraries from the [gql-dart project], including [`gql`], [`gql_link`], and [`normalize`]. We also depend on [hive](https://docs.hivedb.dev/#/) for persistence via `HiveStore`. + +- [GraphQL Client](#graphql-client) + - [Installation](#installation) + - [Migration Guide](#migration-guide) + - [Basic Usage](#basic-usage) + - [Persistence](#persistence) + - [Query](#query) + - [Mutations](#mutations) + - [GraphQL Upload](#graphql-upload) + - [Subscriptions](#subscriptions) + - [`client.watchQuery` and `ObservableQuery`](#clientwatchquery-and-observablequery) + - [Direct Cache Access API](#direct-cache-access-api) + - [Policies](#policies) + - [`FetchPolicy` determines where the client may return a result from.](#fetchpolicy-determines-where-the-client-may-return-a-result-from) + - [`ErrorPolicy` determines the level of events for errors in the execution result.](#errorpolicy-determines-the-level-of-events-for-errors-in-the-execution-result) + - [Exceptions](#exceptions) + - [Links](#links) + - [Composing Links](#composing-links) + - [AWS AppSync Support](#aws-appsync-support) + - [Cognito Pools](#cognito-pools) + - [Other Authorization Types](#other-authorization-types) + - [Parsing ASTs at build-time](#parsing-asts-at-build-time) + - [`PersistedQueriesLink` (experimental) :warning: OUT OF SERVICE :warning:](#persistedquerieslink-experimental-warning-out-of-service-warning) + +**Useful API Docs:** + +- [`GraphQLCache`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLCache-class.html) +- [`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) (direct cache access) + ## Installation First, depend on this package: @@ -31,35 +63,60 @@ import 'package:graphql/client.dart'; Find the migration from version 3 to version 4 [here](./../../changelog-v3-v4.md). -## Usage +## Basic Usage To connect to a GraphQL Server, we first need to create a `GraphQLClient`. A `GraphQLClient` requires both a `cache` and a `link` to be initialized. -In our example below, we will be using the Github Public API. we are going to use `HttpLink` which we will concatenate with `AuthLink` so as to attach our github access token. For the cache, we are going to use `GraphQLCache`. +In our example below, we will be using the Github Public API. we are going to use `HttpLink` which we will concatenate with `AuthLink` so as to attach our github access token. +For the cache, we are going to use `GraphQLCache`. ```dart // ... -final HttpLink _httpLink = HttpLink( +final _httpLink = HttpLink( 'https://api.github.com/graphql', ); -final AuthLink _authLink = AuthLink( +final _authLink = AuthLink( getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN', ); -final Link _link = _authLink.concat(_httpLink); +Link _link = _authLink.concat(_httpLink); -final GraphQLClient _client = GraphQLClient( - cache: GraphQLCache( - // The default store is the InMemoryStore, which does NOT persist to disk - store: HiveStore(), - ), +/// subscriptions must be split otherwise `HttpLink` will. swallow them +if (websocketEndpoint != null){ + final _wsLink = WebSocketLink(websockeEndpoint); + _link = Link.split((request) => request.isSubscription, _wsLink, _link); +} + +final GraphQLClient client = GraphQLClient( + /// **NOTE** The default store is the InMemoryStore, which does NOT persist to disk + cache: GraphQLCache(), link: _link, ); // ... +``` + +### Persistence +In `v4`, `GraphQLCache` is decoupled from persistence, which is managed (or not) by its `store` argument. +We provide a `HiveStore` for easily using [hive](https://docs.hivedb.dev/#/) boxes as storage, +which requires a few changes to the above: + +> **NB**: This is different in `graphql_flutter`, which provides `await initHiveForFlutter()` for initialization in `main` + +```dart +GraphQL getClient() async { + ... + /// initialize Hive and wrap the default box in a HiveStore + final store = await HiveStore.open(path: 'my/cache/path'); + return GraphQLClient( + /// pass the store to the cache for persistence + cache: GraphQLCache(store: store), + link: _link, + ); +} ``` Once you have initialized a client, you can run queries and mutations. @@ -109,7 +166,7 @@ And finally you can send the query to the server and `await` the response: ```dart // ... -final QueryResult result = await _client.query(options); +final QueryResult result = await client.query(options); if (result.hasException) { print(result.exception.toString()); @@ -123,7 +180,7 @@ final List repositories = ### Mutations -Creating a Mutation is also similar to creating a query, with a small difference. First, start with a multiline string: +Creating a mutation is similar to creating a query, with a small difference. First, start with a multiline string: ```dart const String addStar = r''' @@ -152,12 +209,12 @@ final MutationOptions options = MutationOptions( // ... ``` -And finally you can send the query to the server and `await` the response: +And finally you can send the mutation to the server and `await` the response: ```dart // ... -final QueryResult result = await _client.mutate(options); +final QueryResult result = await client.mutate(options); if (result.hasException) { print(result.exception.toString()); @@ -175,36 +232,228 @@ if (isStarred) { // ... ``` -## Links +#### GraphQL Upload + +[gql_http_link](https://pub.dev/packages/gql_http_link) provides support for the GraphQL Upload spec as proposed at +https://github.com/jaydenseric/graphql-multipart-request-spec + +```graphql +mutation($files: [Upload!]!) { + multipleUpload(files: $files) { + id + filename + mimetype + path + } +} +``` + +```dart +import "package:http/http.dart" show Multipartfile; + +// ... + +final myFile = MultipartFile.fromString( + "", + "just plain text", + filename: "sample_upload.txt", + contentType: MediaType("text", "plain"), +); + +final result = await graphQLClient.mutate( + MutationOptions( + document: gql(uploadMutation), + variables: { + 'files': [myFile], + }, + ) +); +``` + +### Subscriptions + +To use subscriptions, a subscription-consuming link **must** be split from your `HttpLink` or other terminating link route: + +```dart +link = Link.split((request) => request.isSubscription, websocketLink, link); +``` + +Then you can `subscribe` to any `subscription`s provided by your server schema: + +```dart +final subscriptionDocument = gql( + r''' + subscription reviewAdded { + reviewAdded { + stars, commentary, episode + } + } + ''', +); +// graphql/client.dart usage +subscription = client.subscribe( + SubscriptionOptions( + document: subscriptionDocument + ), +); +subscription.listen(reactToAddedReview) +``` + +### `client.watchQuery` and `ObservableQuery` + +[`client.watchQuery`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLClient/watchQuery.html) +can be used to execute both queries and mutations, then reactively listen to changes to the underlying data in the cache. It is used in the `Query` and `Mutation` widgets of `graphql_flutter`: + +```dart +final observableQuery = client.watchQuery( + WatchQueryOptions( + document: gql( + r''' + query HeroForEpisode($ep: Episode!) { + hero(episode: $ep) { + name + } + } + ''', + ), + variables: {'ep': 'NEWHOPE'}, + ), +); -`graphql` and `graphql_flutter` now use the [`gql_link`] system, re-exporting: -* [gql_http_link](https://pub.dev/packages/gql_http_link) -* [gql_error_link](https://pub.dev/packages/gql_error_link) -* [gql_dedupe_link](https://pub.dev/packages/gql_dedupe_link) -* [gql_link](https://pub.dev/packages/gql_link) +/// Listen to the stream of results. This will include: +/// * `options.optimisitcResult` if passed +/// * The result from the server (if `options.fetchPolicy` includes networking) +/// * rebroadcast results from edits to the cache +observableQuery.stream.listen((QueryResult result) { + if (!result.isLoading && result.data != null) { + if (result.hasException) { + print(result.exception); + return; + } + if (result.isLoading) { + print('loading'); + return; + } + doSomethingWithMyQueryResult(myCustomParser(result.data)); + } +}); +// ... cleanup: +observableQuery.close(); +``` + +`ObservableQuery` is a bit of a kitchen sink for reactive operation logic – consider looking at the [API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/ObservableQuery-class.html) if you'd like to develop a deeper understanding. + +> **NB**: `watchQuery` and `ObservableQuery` currently don't have a nice APIs for `update` `onCompleted` and `onError` callbacks, +> but you can have a look at how `graphql_flutter` registers them through +> [`onData`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/ObservableQuery/onData.html) in +> [`Mutation.runMutation`](https://pub.dev/documentation/graphql_flutter/4.0.0-alpha.7/graphql_flutter/MutationState/runMutation.html). + +## Direct Cache Access API + +The [`GraphQLCache`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLCache-class.html) +leverages [`normalize`] to give us a fairly apollo-ish [direct cache access] API, which is also available on `GraphQLClient`. +This means we can do [local state management] in a similar fashion as well. + +A complete and well-commented rundown of can be found in the +[`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) + +> **NB** You likely want to call the cache access API from your `client` for automatic broadcasting support. + +## Policies + +Policies are used to configure execution and error behavior for a given request. +The client's default policies can also be set for each method via the `defaultPolicies` option. + +#### `FetchPolicy` determines where the client may return a result from. + +Possible options: -As well as our own custom `WebSocketLink` and `AuthLink` +- cacheFirst (default): return result from cache. Only fetch from network if cached result is not available. +- cacheAndNetwork: return result from cache first (if it exists), then return network result once it's available. +- cacheOnly: return result from cache if available, fail otherwise. +- noCache: return result from network, fail if network call doesn't succeed, don't save to cache. +- networkOnly: return result from network, fail if network call doesn't succeed, save to cache. + +#### `ErrorPolicy` determines the level of events for errors in the execution result. + +Possible options: + +- none (default): Any GraphQL Errors are treated the same as network errors and any data is ignored from the response. +- ignore: Ignore allows you to read any data that is returned alongside GraphQL Errors, + but doesn't save the errors or report them to your UI. +- all: Using the all policy is the best way to notify your users of potential issues while still showing as much data as possible from your server. + It saves both data and errors into the Apollo Cache so your UI can use them. + +## Exceptions + +If there were problems encountered during a query or mutation, the `QueryResult` will have an `OperationException` in the `exception` field: + +```dart +/// Container for both [graphqlErrors] returned from the server +/// and any [linkException] that caused a failure. +class OperationException implements Exception { + /// Any graphql errors returned from the operation + List graphqlErrors = []; + + /// Errors encountered during execution such as network or cache errors + LinkException linkException; +} +``` + +Example usage: + +```dart +if (result.hasException) { + if (result.exception.linkException is NetworkException) { + // handle network issues, maybe + } + return Text(result.exception.toString()) +} +``` + +## Links + +`graphql` and `graphql_flutter` now use the [`gql_link`] system, re-exporting +[gql_http_link](https://pub.dev/packages/gql_http_link), +[gql_error_link](https://pub.dev/packages/gql_error_link), +[gql_dedupe_link](https://pub.dev/packages/gql_dedupe_link), +and the api from [gql_link](https://pub.dev/packages/gql_link), +as well as our own custom `WebSocketLink` and `AuthLink`. ### Composing Links -The [`gql_link`] systm has a well-specified routing system: + +> **NB**: `WebSocketLink` and other "terminating links" must be used with `split` when there are multiple terminating links. + +The [`gql_link`] systm has a well-specified routing system: ![diagram](https://github.com/gql-dart/gql/blob/master/links/gql_link/assets/gql_link.svg) -A quick rundown of the composition api: +a rundown of the composition api: ```dart +// kitchen sink: Link.from([ // common links run before every request - AuthLink(getToken: commonAuthenticator), DedupeLink(), // dedupe requests ErrorLink(onException: reportClientException), ]).split( // split terminating links, or they will break (request) => request.isSubscription, MyCustomSubscriptionAuthLink().concat( - WebsocketLink(mySubscriptionEndpoint), - ), - HttpLink(myAppEndpoint), + WebSocketLink(mySubscriptionEndpoint), + ), // MyCustomSubscriptionAuthLink is only applied to subscriptions + AuthLink(getToken: httpAuthenticator).concat( + HttpLink(myAppEndpoint), + ) ); // adding links after here would be pointless, as they would never be accessed + +/// both `Link.from` and `link.concat` can be used to chain links: +final Link _link = _authLink.concat(_httpLink); +final Link _link = Link.from([_authLink, _httpLink]); + +/// `Link.split` and `link.split` route requests to the left or right based on some condition +/// for instance, if you do `authLink.concat(httpLink).concat(websocketLink)`, +/// `websocketLink` won't see any `subscriptions` +link = Link.split((request) => request.isSubscription, websocketLink, link); ``` When combining links, **it isimportant to note that**: @@ -212,28 +461,30 @@ When combining links, **it isimportant to note that**: - Terminating links like `HttpLink` and `WebsocketLink` must come at the end of a route, and will not call links following them. - Link order is very important. In `HttpLink(myEndpoint).concat(AuthLink(getToken: authenticate))`, the `AuthLink` will never be called. -#### Using Concat +### AWS AppSync Support -```dart -final Link _link = _authLink.concat(_httpLink); -``` +#### Cognito Pools -#### Using Links.from - -`Link.from` joins multiple links into a single link at once. +To use with an AppSync GraphQL API that is authorized with AWS Cognito User Pools, simply pass the JWT token for your Cognito user session in to the `AuthLink`: ```dart -final Link _link = Link.from([_authLink, _httpLink]); +// Where `session` is a CognitorUserSession +// from amazon_cognito_identity_dart_2 +final token = session.getAccessToken().getJwtToken(); + +final AuthLink authLink = AuthLink( + getToken: () => token, +); ``` -#### Using Links.split +See more: [Issue #209](https://github.com/zino-app/graphql-flutter/issues/209) -`Link.split` routes the request based on some condition. -**NB**: `WebSocketLink` and other "terminating links" must be used with `split` when there are multiple. +#### Other Authorization Types -```dart -link = Link.split((request) => request.isSubscription, websocketLink, link); -``` +API key, IAM, and Federated provider authorization could be accomplished through custom links, but it is not known to be supported. Anyone wanting to implement this can reference AWS' JS SDK `AuthLink` implementation. + +- Making a custom link: [Comment on Issue 173](https://github.com/zino-app/graphql-flutter/issues/173#issuecomment-464435942) +- AWS JS SDK `auth-link.ts`: [aws-mobile-appsync-sdk-js:auth-link.ts](https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/master/packages/aws-appsync-auth-link/src/auth-link.ts) ## Parsing ASTs at build-time @@ -274,8 +525,7 @@ final MutationOptions options = MutationOptions( // ... ``` - -### `PersistedQueriesLink` (experimental) :warning: OUT OF SERVICE :warning: +## `PersistedQueriesLink` (experimental) :warning: OUT OF SERVICE :warning: **NOTE**: There is [a PR](https://github.com/zino-app/graphql-flutter/pull/699) for migrating the `v3` `PersistedQueriesLink`, and it works, but requires more consideration. It will be fixed before `v4` `stable` is published @@ -292,7 +542,7 @@ final PersistedQueriesLink _apqLink = PersistedQueriesLink( ); final HttpLink _httpLink = HttpLink( - uri: 'https://api.url/graphql', + 'https://api.url/graphql', ); final Link _link = _apqLink.concat(_httpLink); @@ -314,7 +564,14 @@ final Link _link = _apqLink.concat(_httpLink); [github-star-link]: https://github.com/zino-app/graphql-flutter/stargazers [discord-badge]: https://img.shields.io/discord/559455668810153989.svg?style=flat-square&logo=discord&logoColor=ffffff [discord-link]: https://discord.gg/tXTtBfC +[gql-dart project]: https://github.com/gql-dart [`gql_link`]: https://github.com/gql-dart/gql/tree/master/links/gql_link +[`gql`]: https://github.com/gql-dart/gql/tree/master/gql +[`normalize`]: https://github.com/gql-dart/ferry/tree/master/normalize [apollo]: https://www.apollographql.com/ -[Automatic persisted queries]: https://www.apollographql.com/docs/apollo-server/performance/apq/ -[Apollo's GraphQL Server]: https://www.apollographql.com/docs/apollo-server/ +[apollo client]: https://www.apollographql.com/docs/react/ +[automatic persisted queries]: https://www.apollographql.com/docs/apollo-server/performance/apq/ +[apollo's graphql server]: https://www.apollographql.com/docs/apollo-server/ +[local state management]: https://www.apollographql.com/docs/tutorial/local-state/#update-local-data +[`typepolicies`]: https://www.apollographql.com/docs/react/caching/cache-configuration/#the-typepolicy-type +[direct cache access]: https://www.apollographql.com/docs/react/caching/cache-interaction/ diff --git a/packages/graphql/lib/src/cache/data_proxy.dart b/packages/graphql/lib/src/cache/data_proxy.dart index 255656f46..0038b1d07 100644 --- a/packages/graphql/lib/src/cache/data_proxy.dart +++ b/packages/graphql/lib/src/cache/data_proxy.dart @@ -1,16 +1,100 @@ -import "package:meta/meta.dart"; - import 'package:gql_exec/gql_exec.dart' show Request; -import 'package:gql/ast.dart' show DocumentNode; import './fragment.dart'; +// massive example taken from 'all methods with exposition' in graphql_client_test /// A proxy to the normalized data living in our store. /// /// This interface allows a user to read and write /// denormalized data which feels natural to the user /// whilst in the background this data is being converted /// into the normalized store format. +/// +/// Here is a complete and expository rundown of the usage of [GraphQLDataProxy]'s methods +/// ([readQuery], [writeQuery], [readFragment], [writeFragment]), given an instance of `GraphQLClient client` as an example +/// ```dart +/// /// entity identifiers for normalization +/// final idFields = {'__typename': 'MyType', 'id': 1}; +/// +/// /// The direct cache API uses `gql_link` Requests directly +/// /// These can also be obtained via `options.asRequest` from any `Options` object, +/// /// or via `Operation(document: gql(...)).asRequest()` +/// final queryRequest = Request( +/// operation: Operation( +/// document: gql( +/// r'''{ +/// someField { +/// __typename, +/// id, +/// myField +/// } +/// }''', +/// ), +/// ), +/// ); +/// +/// final queryData = { +/// 'someField': { +/// ...idFields, +/// 'myField': 'originalValue', +/// }, +/// }; +/// +/// /// `broadcast: true` (the default) would rebroadcast cache updates to all safe instances of `ObservableQuery` +/// /// **NOTE**: only `GraphQLClient` can immediately call for a query rebroadcast. if you request a rebroadcast directly +/// /// from the cache, it still has to wait for the client to check in on it +/// client.writeQuery(queryRequest, data: queryData, broadcast: false); +/// +/// /// `optimistic: true` (the default) integrates optimistic data +/// /// written to the cache into your read. +/// expect( +/// client.readQuery(queryRequest, optimistic: false), equals(queryData)); +/// +/// /// While fragments are never executed themselves, we provide a `gql_link`-like API for consistency. +/// /// These can also be obtained via `Fragment(document: gql(...)).asRequest()`. +/// final fragmentRequest = FragmentRequest( +/// fragment: Fragment( +/// document: gql( +/// r''' +/// fragment mySmallSubset on MyType { +/// myField, +/// someNewField +/// } +/// ''', +/// ), +/// ), +/// idFields: idFields); +/// +/// /// We've specified `idFields` and are only editing a subset of the data +/// final fragmentData = { +/// 'myField': 'updatedValue', +/// 'someNewField': [ +/// {'newData': false} +/// ], +/// }; +/// +/// /// We didn't disable `broadcast`, so all instances of `ObservableQuery` will be notified of any changes +/// client.writeFragment(fragmentRequest, data: fragmentData); +/// +/// /// __typename is automatically included in all reads +/// expect( +/// client.readFragment(fragmentRequest), +/// equals({ +/// '__typename': 'MyType', +/// ...fragmentData, +/// }), +/// ); +/// +/// final updatedQueryData = { +/// 'someField': { +/// ...idFields, +/// 'myField': 'updatedValue', +/// }, +/// }; +/// +/// /// `myField` is updated, but we don't have `someNewField`, as expected. +/// expect(client.readQuery(queryRequest), equals(updatedQueryData)); +/// ``` abstract class GraphQLDataProxy { /// Reads a GraphQL query from the root query id. Map readQuery(Request request, {bool optimistic}); diff --git a/packages/graphql/lib/src/core/policies.dart b/packages/graphql/lib/src/core/policies.dart index 03023da93..bd1df5bf6 100644 --- a/packages/graphql/lib/src/core/policies.dart +++ b/packages/graphql/lib/src/core/policies.dart @@ -32,7 +32,6 @@ bool shouldStopAtCache(FetchPolicy fetchPolicy) => /// but doesn't save the errors or report them to your UI. /// - all: Using the all policy is the best way to notify your users of potential issues while still showing as much data as possible from your server. /// It saves both data and errors into the Apollo Cache so your UI can use them. - enum ErrorPolicy { none, ignore, diff --git a/packages/graphql/lib/src/exceptions/exceptions_next.dart b/packages/graphql/lib/src/exceptions/exceptions_next.dart index 15b15a2ba..cda4bdb71 100644 --- a/packages/graphql/lib/src/exceptions/exceptions_next.dart +++ b/packages/graphql/lib/src/exceptions/exceptions_next.dart @@ -31,6 +31,8 @@ class UnknownException extends LinkException { ) : super(originalException); } +/// Container for both [graphqlErrors] returned from the server +/// and any [linkException] that caused a failure. class OperationException implements Exception { /// Any graphql errors returned from the operation List graphqlErrors = []; diff --git a/packages/graphql/lib/src/graphql_client.dart b/packages/graphql/lib/src/graphql_client.dart index 8387db5c8..a032121e6 100644 --- a/packages/graphql/lib/src/graphql_client.dart +++ b/packages/graphql/lib/src/graphql_client.dart @@ -200,13 +200,13 @@ class GraphQLClient implements GraphQLDataProxy { ); /// pass through to [cache.readQuery] - readQuery(request, {optimistic}) => + readQuery(request, {optimistic = true}) => cache.readQuery(request, optimistic: optimistic); /// pass through to [cache.readFragment] readFragment( fragmentRequest, { - optimistic, + optimistic = true, }) => cache.readFragment( fragmentRequest, @@ -214,7 +214,7 @@ class GraphQLClient implements GraphQLDataProxy { ); /// pass through to [cache.writeQuery] and then rebroadcast any changes. - void writeQuery(request, {data, broadcast}) { + void writeQuery(request, {data, broadcast = true}) { cache.writeQuery(request, data: data, broadcast: broadcast); queryManager.maybeRebroadcastQueries(); } @@ -222,7 +222,7 @@ class GraphQLClient implements GraphQLDataProxy { /// pass through to [cache.writeFragment] and then rebroadcast any changes. void writeFragment( fragmentRequest, { - broadcast, + broadcast = true, data, }) { cache.writeFragment( diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index f8aa933d1..18062aec7 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: gql_dedupe_link: ^1.0.10 hive: ^1.3.0 - normalize: ^0.2.0 + normalize: ^0.2.4 http: ^0.12.1 collection: ^1.14.12 diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index 4a55ffbe5..e1fb17701 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -379,4 +379,99 @@ void main() { }); }); }); + + group('direct cache access', () { + setUp(() { + link = MockLink(); + + client = GraphQLClient( + cache: getTestCache(), + link: link, + ); + }); + + test('all methods with exposition', () { + /// entity identifiers for normalization + final idFields = {'__typename': 'MyType', 'id': 1}; + + /// The direct cache API uses `gql_link` Requests directly + /// These can also be obtained via `options.asRequest` from any `Options` object, + /// or via `Operation(document: gql(...)).asRequest()` + final queryRequest = Request( + operation: Operation( + document: gql( + r'''{ + someField { + __typename, + id, + myField + } + }''', + ), + ), + ); + + final queryData = { + 'someField': { + ...idFields, + 'myField': 'originalValue', + }, + }; + + /// `broadcast: true` (the default) would rebroadcast cache updates to all safe instances of `ObservableQuery` + /// **NOTE**: only `GraphQLClient` can immediately call for a query rebroadcast. if you request a rebroadcast directly + /// from the cache, it still has to wait for the client to check in on it + client.writeQuery(queryRequest, data: queryData, broadcast: false); + + /// `optimistic: true` (the default) integrates optimistic data + /// written to the cache into your read. + expect( + client.readQuery(queryRequest, optimistic: false), equals(queryData)); + + /// While fragments are never executed themselves, we provide a `gql_link`-like API for consistency. + /// These can also be obtained via `Fragment(document: gql(...)).asRequest()`. + final fragmentRequest = FragmentRequest( + fragment: Fragment( + document: gql( + r''' + fragment mySmallSubset on MyType { + myField, + someNewField + } + ''', + ), + ), + idFields: idFields); + + /// We've specified `idFields` and are only editing a subset of the data + final fragmentData = { + 'myField': 'updatedValue', + 'someNewField': [ + {'newData': false} + ], + }; + + /// We didn't disable `broadcast`, so all instances of `ObservableQuery` will be notified of any changes + client.writeFragment(fragmentRequest, data: fragmentData); + + /// __typename is automatically included in all reads + expect( + client.readFragment(fragmentRequest), + equals({ + '__typename': 'MyType', + ...fragmentData, + }), + ); + + final updatedQueryData = { + 'someField': { + ...idFields, + 'myField': 'updatedValue', + }, + }; + + /// `myField` is updated, but we don't have `someNewField`, as expected. + expect(client.readQuery(queryRequest), equals(updatedQueryData)); + }); + }); } diff --git a/packages/graphql_flutter/README.md b/packages/graphql_flutter/README.md index ed01d17aa..d32459f83 100644 --- a/packages/graphql_flutter/README.md +++ b/packages/graphql_flutter/README.md @@ -12,34 +12,46 @@ # GraphQL Flutter -## Table of Contents - -- [Installation](#installation) -- [Usage](#usage) - - [GraphQL Provider](#graphql-provider) - - [Offline Cache](#offline-cache) - - [Normalization](#normalization) - - [Optimism](#optimism) - - [Queries](#queries) - - [Fetch More (Pagination)](#fetch-more-pagination) - - [Mutations](#mutations) - - [Mutations with optimism](#mutations-with-optimism) - - [Exceptions](#exceptions) - - [Subscriptions (Experimental)](#subscriptions-experimental) - - [GraphQL Consumer](#graphql-consumer) - - [GraphQL Upload](#graphql-upload) -- [Roadmap](#roadmap) +`graphql_flutter` provides an idiomatic flutter API and widgets for [`graphql/client.dart`](https://pub.dev/packages/graphql). They are co-developed [on github](https://github.com/zino-app/graphql-flutter), where you can find more in-depth examples. We also have a lively community on [discord][discord-link]. + +This guide is mostly focused on setup, widgets, and flutter-specific considerations. For more in-depth details on the `graphql` API, see the [`graphql` README](../graphql/README.md) + +- [GraphQL Flutter](#graphql-flutter) + - [Installation](#installation) + - [Migration Guide](#migration-guide) + - [Usage](#usage) + - [GraphQL Provider](#graphql-provider) + - [Query](#query) + - [Fetch More (Pagination)](#fetch-more-pagination) + - [Mutations](#mutations) + - [Optimism](#optimism) + - [Subscriptions](#subscriptions) + - [GraphQL Consumer](#graphql-consumer) + +**Useful sections in the [`graphql` README](../graphql/README.md):** + +- [in-depth link guide](../graphql/README.md#links) +- [Policies](../graphql/README.md#exceptions) +- [Exceptions](../graphql/README.md#exceptions) +- [AWS AppSync Support](../graphql/README.md#aws-appsync-support) +- [GraphQL Upload](../graphql/README.md#graphql-upload) +- [Parsing ASTs at build-time](../graphql/README.md##parsing-asts-at-build-time) + +**Useful API Docs:** + +- [`GraphQLCache`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLCache-class.html) +- [`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) (direct cache access) ## Installation -First, depends on the library by adding this to your packages `pubspec.yaml`: +First, depend on this package: ```yaml dependencies: - graphql_flutter: ^3.0.0 + graphql: ^4.0.0-beta ``` -Now inside your Dart code, you can import it. +And then import it inside your dart code: ```dart import 'package:graphql_flutter/graphql_flutter.dart'; @@ -51,18 +63,23 @@ Find the migration from version 2 to version 3 [here](./../../changelog-v2-v3.md ## Usage -To use the client it first needs to be initialized with a link and cache. For this example, we will be using an `HttpLink` as our link and `GraphQLCache` as our cache. If your endpoint requires authentication you can concatenate the `AuthLink`, it resolves the credentials using a future, so you can authenticate asynchronously. +To connect to a GraphQL Server, we first need to create a `GraphQLClient`. A `GraphQLClient` requires both a `cache` and a `link` to be initialized. -> For this example we will use the public GitHub API. +In our example below, we will be using the Github Public API. we are going to use `HttpLink` which we will concatenate with `AuthLink` so as to attach our github access token. For the cache, we are going to use `GraphQLCache`. ```dart ... import 'package:graphql_flutter/graphql_flutter.dart'; -void main() { +void main() async { + + // We're using HiveStore for persistence, + // so we need to initialize Hive. + await initHiveForFlutter(); + final HttpLink httpLink = HttpLink( - uri: 'https://api.github.com/graphql', + 'https://api.github.com/graphql', ); final AuthLink authLink = AuthLink( @@ -75,8 +92,9 @@ void main() { ValueNotifier client = ValueNotifier( GraphQLClient( - cache: GraphQLCache(), link: link, + // The default store is the InMemoryStore, which does NOT persist to disk + store: GraphQLCache(store: HiveStore()), ), ); @@ -88,7 +106,7 @@ void main() { ### GraphQL Provider -In order to use the client, you `Query` and `Mutation` widgets to be wrapped with the `GraphQLProvider` widget. +In order to use the client, your `Query` and `Mutation` widgets must be wrapped with the `GraphQLProvider` widget. > We recommend wrapping your `MaterialApp` with the `GraphQLProvider` widget. @@ -106,99 +124,7 @@ In order to use the client, you `Query` and `Mutation` widgets to be wrapped wit ... ``` -### AWS AppSync Support - -#### Cognito Pools - -To use with an AppSync GraphQL API that is authorized with AWS Cognito User Pools, simply pass the JWT token for your Cognito user session in to the `AuthLink`: - -```dart -// Where `session` is a CognitorUserSession -// from amazon_cognito_identity_dart_2 -final token = session.getAccessToken().getJwtToken(); - -final AuthLink authLink = AuthLink( - getToken: () => token, -); -``` - -See more: [Issue #209](https://github.com/zino-app/graphql-flutter/issues/209) - -#### Other Authorization Types - -API key, IAM, and Federated provider authorization could be accomplished through custom links, but it is not known to be supported. Anyone wanting to implement this can reference AWS' JS SDK `AuthLink` implementation. - -- Making a custom link: [Comment on Issue 173](https://github.com/zino-app/graphql-flutter/issues/173#issuecomment-464435942) -- AWS JS SDK `auth-link.ts`: [aws-mobile-appsync-sdk-js:auth-link.ts](https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/master/packages/aws-appsync-auth-link/src/auth-link.ts) - -### Offline Cache - -The in-memory cache can automatically be saved to and restored from offline storage. Setting it up is as easy as wrapping your app with the `CacheProvider` widget. - -> It is required to place the `CacheProvider` widget is inside the `GraphQLProvider` widget, because `GraphQLProvider` makes client available through the build context. - -```dart -... - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return GraphQLProvider( - client: client, - child: CacheProvider( - child: MaterialApp( - title: 'Flutter Demo', - ... - ), - ), - ); - } -} - -... -``` - -#### Normalization - -To enable [apollo-like normalization](https://www.apollographql.com/docs/react/caching/cache-configuration/#data-normalization), use a `NormalizedInMemoryCache` or `OptimisticCache`: - -```dart -ValueNotifier client = ValueNotifier( - GraphQLClient( - cache: NormalizedInMemoryCache( - dataIdFromObject: typenameDataIdFromObject, - ), - link: link, - ), -); -``` - -`dataIdFromObject` is required and has no defaults. Our implementation is similar to Apollo's, requiring a function to return a universally unique string or `null`. The predefined `typenameDataIdFromObject` we provide is similar to apollo's default: - -```dart -String typenameDataIdFromObject(Object object) { - if (object is Map && - object.containsKey('__typename') && - object.containsKey('id')) { - return "${object['__typename']}/${object['id']}"; - } - return null; -} -``` - -However, note that **`graphql-flutter` does not inject \_\_typename into operations** the way Apollo does, so if you aren't careful to request them in your query, this normalization scheme is not possible. - -Unlike Apollo, we don't have a real client-side document parser and resolver, so **operations leveraging normalization can have additional fields not specified in the query**. There are a couple of ideas for constraining this (leveraging `json_serializable`, or just implementing the resolver), but for now, the normalized cache uses a [`LazyCacheMap`](lib/src/cache/lazy_cache_map.dart), which wraps underlying data with a lazy denormalizer to allow for cyclical references. It has the same API as a normal `HashMap`, but is currently a bit hard to debug with, as a descriptive debug representation is currently unavailable. - -NOTE: A `LazyCacheMap` can be modified, but this does not affect the underlying entities in the cache. If references are added to the map, they will still dereference against the cache normally. - -#### Optimism - -The `OptimisticCache` allows for optimistic mutations by passing an `optimisticResult` to `RunMutation`. It will then call `update(Cache cache, QueryResult result)` twice (once eagerly with `optimisticResult`), and rebroadcast all queries with the optimistic cache. You can tell which entities in the cache are optimistic through the `.isOptimistic` flag on `LazyCacheMap`, though note that **this is only the case for optimistic entities and not their containing operations/maps**. - -`QueryResults` also, have an `optimistic` flag, but I would recommend looking at the data itself, as many situations make it unusable (such as toggling mutations like in the example below). [Mutation usage examples](#mutations-with-optimism) - -### Queries +### Query Creating a query is as simple as creating a multiline string: @@ -228,7 +154,7 @@ Query( variables: { 'nRepositories': 50, }, - pollInterval: 10, + pollInterval: Duration(seconds: 10), ), // Just like in apollo refetch() could be used to manually trigger a refetch // while fetchMore() can be used for pagination purpose @@ -237,7 +163,7 @@ Query( return Text(result.exception.toString()); } - if (result.loading) { + if (result.isLoading) { return Text('Loading'); } @@ -331,7 +257,7 @@ Mutation( options: MutationOptions( document: gql(addStar), // this is the mutation string you just created // you can update the cache based on results - update: (Cache cache, QueryResult result) { + update: (GraphQLDataProxy cache, QueryResult result) { return cache; }, // or do something with the result.data on completion @@ -356,9 +282,14 @@ Mutation( ... ``` -#### Mutations with optimism +`graphql` also provides [file upload](../graphql/README.md#graphql-upload) support as well. -If you're using an [OptimisticCache](#optimism), you can provide an `optimisticResult`: +#### Optimism + +`GraphQLCache` allows for optimistic mutations by passing an `optimisticResult` to `RunMutation`. It will then call `update(GraphQLDataProxy cache, QueryResult result)` twice (once eagerly with `optimisticResult`), and rebroadcast all queries with the optimistic cache state. + +A complete and well-commented rundown of how exactly one interfaces with the `proxy` provided to `update` can be fount in the +[`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) ```dart ... @@ -380,45 +311,43 @@ FlutterWidget( With a bit more context (taken from **[the complete mutation example `StarrableRepository`](example/lib/graphql_widget/main.dart)**): ```dart -// bool get starred => repository['viewerHasStarred'] as bool; -// bool get optimistic => (repository as LazyCacheMap).isOptimistic; +// final Map repository; +// final bool optimistic; +// Map extractRepositoryData(Map data); +// Map get expectedResult; Mutation( options: MutationOptions( document: gql(starred ? mutations.removeStar : mutations.addStar), - // will be called for both optimistic and final results - update: (Cache cache, QueryResult result) { + update: (cache, result) { if (result.hasException) { - print(['optimistic', result.exception.toString()]); + print(result.exception); } else { - final Map updated = - Map.from(repository) - ..addAll(extractRepositoryData(result.data)); - cache.write(typenameDataIdFromObject(updated), updated); + final updated = { + ...repository, + ...extractRepositoryData(result.data), + }; + cache.writeFragment( + Fragment( + document: gql( + ''' + fragment fields on Repository { + id + name + viewerHasStarred + } + ''', + // helper for constructing FragmentRequest + )).asRequest(idFields: { + '__typename': updated['__typename'], + 'id': updated['id'], + }), + data: updated, + broadcast: false, + ); } }, - // will only be called for final result - onCompleted: (dynamic resultData) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text( - extractRepositoryData(resultData)['viewerHasStarred'] as bool - ? 'Thanks for your star!' - : 'Sorry you changed your mind!', - ), - actions: [ - SimpleDialogOption( - child: const Text('Dismiss'), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - ); - }, - ); - }, + onError: (OperationException error) { }, + onCompleted: (dynamic resultData) { }, ), builder: (RunMutation toggleStar, QueryResult result) { return ListTile( @@ -428,83 +357,71 @@ Mutation( color: Colors.amber, ) : const Icon(Icons.star_border), - trailing: result.loading || optimistic + trailing: result.isLoading || optimistic ? const CircularProgressIndicator() : null, title: Text(repository['name'] as String), onTap: () { toggleStar( - { 'starrableId': repository['id'] }, - optimisticResult: { - 'action': { - 'starrable': {'viewerHasStarred': !starred} - } - }, + {'starrableId': repository['id']}, + optimisticResult: expectedResult, ); }, ); }, -); +) ``` -### Exceptions +### Subscriptions -If there were problems encountered during a query or mutation, the `QueryResult` will have an `OperationException` in the `exception` field: +The syntax for subscriptions is again similar to a query, however, it utilizes WebSockets and dart Streams to provide real-time updates from a server. -```dart -class OperationException implements Exception { - /// Any graphql errors returned from the operation - List graphqlErrors = []; - - /// Errors encountered during execution such as network or cache errors - ClientException clientException; -} -``` - -Example usage: +To use subscriptions, a subscription-consuming link **must** be split from your `HttpLink` or other terminating link route: ```dart -if (result.hasException) { - if (result.exception.clientException is NetworkException) { - // handle network issues, maybe - } - return Text(result.exception.toString()) -} +link = Link.split((request) => request.isSubscription, websocketLink, link); ``` -### Subscriptions (Experimental) - -The syntax for subscriptions is again similar to a query, however, this utilizes WebSockets and dart Streams to provide real-time updates from a server. -Before subscriptions can be performed a global instance of `socketClient` needs to be initialized. - -> We are working on moving this into the same `GraphQLProvider` structure as the http client. Therefore this api might change in the near future. +Then you can `subscribe` to any `subscription`s provided by your server schema: ```dart -socketClient = await SocketClient.connect('ws://coolserver.com/graphql'); -``` - -Once the `socketClient` has been initialized it can be used by the `Subscription` `Widget` +final subscriptionDocument = gql( + r''' + subscription reviewAdded { + reviewAdded { + stars, commentary, episode + } + } + ''', +); -```dart class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Subscription( - operationName, - query, - variables: variables, - builder: ({ - bool loading, - dynamic payload, - dynamic error, - }) { - if (payload != null) { - return Text(payload['requestSubscription']['requestData']); - } else { - return Text('Data not found'); + options: SubscriptionOptions( + document: subscriptionDocument, + ), + builder: (result) { + if (result.hasException) { + return Text(result.exception.toString()); + } + + if (result.isLoading) { + return Center( + child: const CircularProgressIndicator(), + ); } + // ResultAccumulator is a provided helper widget for collating subscription results. + // careful though! It is stateful and will discard your results if the state is disposed + return ResultAccumulator.appendUniqueEntries( + latest: result.data, + builder: (context, {results}) => DisplayReviews( + reviews: results.reversed.toList(), + ), + ); } ), ) @@ -515,7 +432,9 @@ class _MyHomePageState extends State { ### GraphQL Consumer -You can always access the client directly from the `GraphQLProvider` but to make it even easier you can also use them `GraphQLConsumer` widget. +If you want to use the `client` directly, say for some its +[direct cache update](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) methods, +You can use `GraphQLConsumer` to grab it from any `context` descended from a `GraphQLProvider`: ```dart ... @@ -533,61 +452,6 @@ You can always access the client directly from the `GraphQLProvider` but to make ... ``` -### GraphQL Upload - -We support GraphQL Upload spec as proposed at -https://github.com/jaydenseric/graphql-multipart-request-spec - -```grapql -mutation($files: [Upload!]!) { - multipleUpload(files: $files) { - id - filename - mimetype - path - } -} -``` - -```dart -import "package:http/http.dart" show Multipartfile; - -// ... - -final myFile = MultipartFile.fromString( - "", - "just plain text", - filename: "sample_upload.txt", - contentType: MediaType("text", "plain"), -); - -final result = await graphQLClient.mutate( - MutationOptions( - document: gql(uploadMutation), - variables: { - 'files': [myFile], - }, - ) -); -``` - -## Roadmap - -This is currently our roadmap, please feel free to request additions/changes. - -| Feature | Progress | -| :---------------------- | :------: | -| Queries | ✅ | -| Mutations | ✅ | -| Subscriptions | ✅ | -| Query polling | ✅ | -| In memory cache | ✅ | -| Offline cache sync | ✅ | -| GraphQL pload | ✅ | -| Optimistic results | ✅ | -| Client state management | 🔜 | -| Modularity | 🔜 | - [build-status-badge]: https://img.shields.io/circleci/build/github/zino-app/graphql-flutter.svg?style=flat-square [build-status-link]: https://circleci.com/gh/zino-app/graphql-flutter [coverage-badge]: https://img.shields.io/codecov/c/github/zino-app/graphql-flutter.svg?style=flat-square diff --git a/packages/graphql_flutter/example/lib/graphql_widget/main.dart b/packages/graphql_flutter/example/lib/graphql_widget/main.dart index 2857eb207..a9daf542d 100644 --- a/packages/graphql_flutter/example/lib/graphql_widget/main.dart +++ b/packages/graphql_flutter/example/lib/graphql_widget/main.dart @@ -188,7 +188,8 @@ class StarrableRepository extends StatelessWidget { ...extractRepositoryData(result.data), }; cache.writeFragment( - fragment: gql( + Fragment( + document: gql( ''' fragment fields on Repository { id @@ -196,11 +197,10 @@ class StarrableRepository extends StatelessWidget { viewerHasStarred } ''', - ), - idFields: { + )).asRequest(idFields: { '__typename': updated['__typename'], 'id': updated['id'], - }, + }), data: updated, broadcast: false, ); From cc2df60e51cde107f3435c271c2071d19a85ab5a Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 25 Sep 2020 16:44:16 -0500 Subject: [PATCH 110/118] 4.0.0-alpha.9 --- packages/graphql/CHANGELOG.md | 37 +++++++++++++++++++ packages/graphql/pubspec.yaml | 2 +- .../graphql/test/graphql_client_test.dart | 11 +++--- packages/graphql_flutter/CHANGELOG.md | 37 +++++++++++++++++++ packages/graphql_flutter/pubspec.yaml | 4 +- 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 19b81ad2b..21ac22079 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,40 @@ +# 4.0.0-alpha.9 (2020-09-25) + +:warning: breaking: the `cache.readFragment / cache.readFragment`` API has been reworked: +```diff +final fragDoc = gql(...); + +final idFields = { '__typename': 'MyType', 'id': 1 } + +final fragmentData = { + 'myField': 'updatedValue', + 'someNewField': [ + {'newData': false} + ], +}; + ++ // or Fragment(document: fragDoc).asRequest(idFields: idFields) ++ final fragmentRequest = FragmentRequest( ++ fragment: Fragment( ++ document: fragDoc, ++ ), ++ idFields: idFields, ++ ); + +cache.writeFragment( +- fragment: fragDoc, +- idFields: idFields, ++ fragmentRequest, + data: fragmentData, +); + +``` +This was done because I (@micimize) wanted to make it more consistent with `cache.readQuery`/`cache.writeQuery` before `beta`. + +* **client**: refactor(client): Fragment and FragmentRequest for more normalized api ([2f04058](https://github.com/zino-app/graphql-flutter/commit/2f04058b0dd2d739cd423ccea616c4574f9cf9eb)) +* **docs**: update docs, add more sections ([00f4a97](https://github.com/zino-app/graphql-flutter/commit/00f4a971fa4b1aa14b568b16b25b31b98ef70a4b)) + + # 4.0.0-alpha.8 (2020-09-24) This was mostly a prep release for the first v4 beta. diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 18062aec7..3bd928908 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.8 +version: 4.0.0-alpha.9 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 diff --git a/packages/graphql/test/graphql_client_test.dart b/packages/graphql/test/graphql_client_test.dart index e1fb17701..e678f6ebe 100644 --- a/packages/graphql/test/graphql_client_test.dart +++ b/packages/graphql/test/graphql_client_test.dart @@ -431,17 +431,18 @@ void main() { /// While fragments are never executed themselves, we provide a `gql_link`-like API for consistency. /// These can also be obtained via `Fragment(document: gql(...)).asRequest()`. final fragmentRequest = FragmentRequest( - fragment: Fragment( - document: gql( - r''' + fragment: Fragment( + document: gql( + r''' fragment mySmallSubset on MyType { myField, someNewField } ''', - ), ), - idFields: idFields); + ), + idFields: idFields, + ); /// We've specified `idFields` and are only editing a subset of the data final fragmentData = { diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index eca3fac82..ce0a05ffb 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,40 @@ +# 4.0.0-alpha.9 (2020-09-25) + +:warning: breaking: the `cache.readFragment / cache.readFragment`` API has been reworked: +```diff +final fragDoc = gql(...); + +final idFields = { '__typename': 'MyType', 'id': 1 } + +final fragmentData = { + 'myField': 'updatedValue', + 'someNewField': [ + {'newData': false} + ], +}; + ++ // or Fragment(document: fragDoc).asRequest(idFields: idFields) ++ final fragmentRequest = FragmentRequest( ++ fragment: Fragment( ++ document: fragDoc, ++ ), ++ idFields: idFields, ++ ); + +cache.writeFragment( +- fragment: fragDoc, +- idFields: idFields, ++ fragmentRequest, + data: fragmentData, +); + +``` +This was done because I (@micimize) wanted to make it more consistent with `cache.readQuery`/`cache.writeQuery` before `beta`. + +* **client**: refactor(client): Fragment and FragmentRequest for more normalized api ([2f04058](https://github.com/zino-app/graphql-flutter/commit/2f04058b0dd2d739cd423ccea616c4574f9cf9eb)) +* **docs**: update docs, add more sections ([00f4a97](https://github.com/zino-app/graphql-flutter/commit/00f4a971fa4b1aa14b568b16b25b31b98ef70a4b)) + + # 4.0.0-alpha.8 (2020-09-24) This was mostly a prep release for the first v4 beta. diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index a225c3ea0..8c12295e1 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,10 +2,10 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.8 +version: 4.0.0-alpha.9 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.8 + graphql: ^4.0.0-alpha.9 #path: ../graphql gql_exec: ^0.2.2 flutter: From e96363c685cce0507f81299f76c274d085c5b693 Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 25 Sep 2020 16:46:52 -0500 Subject: [PATCH 111/118] typos --- packages/graphql/CHANGELOG.md | 27 +++++++++++++-------------- packages/graphql_flutter/CHANGELOG.md | 27 +++++++++++++-------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 21ac22079..8f95adb69 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,17 +1,17 @@ # 4.0.0-alpha.9 (2020-09-25) -:warning: breaking: the `cache.readFragment / cache.readFragment`` API has been reworked: +:warning: breaking: the `cache.readFragment / cache.readFragment` API has been reworked: ```diff -final fragDoc = gql(...); + final fragDoc = gql(...); -final idFields = { '__typename': 'MyType', 'id': 1 } + final idFields = { '__typename': 'MyType', 'id': 1 } -final fragmentData = { - 'myField': 'updatedValue', - 'someNewField': [ - {'newData': false} - ], -}; + final fragmentData = { + 'myField': 'updatedValue', + 'someNewField': [ + {'newData': false} + ], + }; + // or Fragment(document: fragDoc).asRequest(idFields: idFields) + final fragmentRequest = FragmentRequest( @@ -21,13 +21,12 @@ final fragmentData = { + idFields: idFields, + ); -cache.writeFragment( + cache.writeFragment( - fragment: fragDoc, - idFields: idFields, -+ fragmentRequest, - data: fragmentData, -); - ++ fragmentRequest, + data: fragmentData, + ); ``` This was done because I (@micimize) wanted to make it more consistent with `cache.readQuery`/`cache.writeQuery` before `beta`. diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index ce0a05ffb..76360f7b4 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,17 +1,17 @@ # 4.0.0-alpha.9 (2020-09-25) -:warning: breaking: the `cache.readFragment / cache.readFragment`` API has been reworked: +:warning: breaking: the `cache.readFragment / cache.readFragment` API has been reworked: ```diff -final fragDoc = gql(...); + final fragDoc = gql(...); -final idFields = { '__typename': 'MyType', 'id': 1 } + final idFields = { '__typename': 'MyType', 'id': 1 } -final fragmentData = { - 'myField': 'updatedValue', - 'someNewField': [ - {'newData': false} - ], -}; + final fragmentData = { + 'myField': 'updatedValue', + 'someNewField': [ + {'newData': false} + ], + }; + // or Fragment(document: fragDoc).asRequest(idFields: idFields) + final fragmentRequest = FragmentRequest( @@ -21,13 +21,12 @@ final fragmentData = { + idFields: idFields, + ); -cache.writeFragment( + cache.writeFragment( - fragment: fragDoc, - idFields: idFields, -+ fragmentRequest, - data: fragmentData, -); - ++ fragmentRequest, + data: fragmentData, + ); ``` This was done because I (@micimize) wanted to make it more consistent with `cache.readQuery`/`cache.writeQuery` before `beta`. From 38b2fd2abbb7e3439e35d139538816beb9e10c40 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 26 Sep 2020 09:41:19 -0500 Subject: [PATCH 112/118] hotfix: bool json encoding support --- packages/graphql/pubspec.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 3bd928908..cb59bc077 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,19 +2,18 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.9 +version: 4.0.0-alpha.10 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 path: ^1.6.2 - gql: ^0.12.0 + gql: ^0.12.3 gql_exec: ^0.2.4 gql_link: ^0.3.0 - gql_http_link: ^0.3.0 - #gql_websocket_link: ^0.1.1-alpha+1596210062684 + gql_http_link: ^0.3.1 gql_transform_link: ^0.1.5 - gql_error_link: ^0.1.1-alpha + gql_error_link: ^0.1.0 gql_dedupe_link: ^1.0.10 hive: ^1.3.0 From 43d8cc4061de711dabafd57c457639cabb5c0d85 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 26 Sep 2020 09:43:22 -0500 Subject: [PATCH 113/118] 4.0.0-alpha.10 --- packages/graphql/CHANGELOG.md | 5 +++++ packages/graphql_flutter/CHANGELOG.md | 5 +++++ packages/graphql_flutter/pubspec.yaml | 4 ++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 8f95adb69..35e35f717 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.10 (2020-09-26) + +* **hotfix(client)**: `gql_http_link==0.3.1` fix for bool json encoding support ([38b2fd2](https://github.com/zino-app/graphql-flutter/commit/38b2fd2abbb7e3439e35d139538816beb9e10c40 )) + + # 4.0.0-alpha.9 (2020-09-25) :warning: breaking: the `cache.readFragment / cache.readFragment` API has been reworked: diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index 76360f7b4..832439847 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.10 (2020-09-26) + +* **hotfix(client)**: `gql_http_link==0.3.1` fix for bool json encoding support ([38b2fd2](https://github.com/zino-app/graphql-flutter/commit/38b2fd2abbb7e3439e35d139538816beb9e10c40 )) + + # 4.0.0-alpha.9 (2020-09-25) :warning: breaking: the `cache.readFragment / cache.readFragment` API has been reworked: diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index 8c12295e1..df01c3b4e 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,10 +2,10 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.9 +version: 4.0.0-alpha.10 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.9 + graphql: ^4.0.0-alpha.10 #path: ../graphql gql_exec: ^0.2.2 flutter: From 98b8cf771e9f982741d5041bd4a4f017ac46dc91 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 26 Sep 2020 11:34:51 -0500 Subject: [PATCH 114/118] fix(client): gql_http_link==0.3.2 for custom toJsons closing #734 --- packages/graphql/lib/src/cache/fragment.dart | 13 +++++++++---- packages/graphql/lib/src/core/policies.dart | 3 +++ packages/graphql/pubspec.yaml | 4 ++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/graphql/lib/src/cache/fragment.dart b/packages/graphql/lib/src/cache/fragment.dart index c6ce9de7b..5c3fb9be2 100644 --- a/packages/graphql/lib/src/cache/fragment.dart +++ b/packages/graphql/lib/src/cache/fragment.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; +import "package:meta/meta.dart"; import "package:collection/collection.dart"; + import "package:gql/ast.dart"; -import 'package:graphql/client.dart'; -import "package:meta/meta.dart"; +import 'package:gql/language.dart'; +import "package:gql_exec/gql_exec.dart"; /// A fragment in a [document], optionally defined by [fragmentName] @immutable @@ -43,8 +46,10 @@ class Fragment { ); @override - String toString() => - "Fragment(document: $document, fragmentName: $fragmentName)"; + String toString() { + final documentRepr = json.encode(printNode(document)); + return "Fragment(document: DocumentNode($documentRepr), fragmentName: $fragmentName)"; + } /// helper for building a [FragmentRequest] @experimental diff --git a/packages/graphql/lib/src/core/policies.dart b/packages/graphql/lib/src/core/policies.dart index bd1df5bf6..2573371fd 100644 --- a/packages/graphql/lib/src/core/policies.dart +++ b/packages/graphql/lib/src/core/policies.dart @@ -76,6 +76,9 @@ class Policies { int get hashCode => const ListEquality( DeepCollectionEquality(), ).hash([fetch, error]); + + @override + String toString() => 'Policies(fetch: $fetch, error: $error)'; } /// The default [Policies] to set for each client action diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index cb59bc077..4a9361a8d 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -10,8 +10,8 @@ dependencies: gql: ^0.12.3 gql_exec: ^0.2.4 - gql_link: ^0.3.0 - gql_http_link: ^0.3.1 + gql_link: ^0.3.1 + gql_http_link: ^0.3.2 gql_transform_link: ^0.1.5 gql_error_link: ^0.1.0 gql_dedupe_link: ^1.0.10 From 715e6a45e7094284c5cc51552ea1816937eafed1 Mon Sep 17 00:00:00 2001 From: micimize Date: Sat, 26 Sep 2020 11:36:45 -0500 Subject: [PATCH 115/118] hotpatch --- packages/graphql/CHANGELOG.md | 5 +++++ packages/graphql/pubspec.yaml | 2 +- packages/graphql_flutter/CHANGELOG.md | 5 +++++ packages/graphql_flutter/pubspec.yaml | 4 ++-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 35e35f717..b553645a9 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.11 (2020-09-26) + +* **hotfix(client)**: `gql_http_link==0.3.2` for custom toJsons closing #734 ([98b8cf7](https://github.com/zino-app/graphql-flutter/commit/98b8cf771e9f982741d5041bd4a4f017ac46dc91)) + + # 4.0.0-alpha.10 (2020-09-26) * **hotfix(client)**: `gql_http_link==0.3.1` fix for bool json encoding support ([38b2fd2](https://github.com/zino-app/graphql-flutter/commit/38b2fd2abbb7e3439e35d139538816beb9e10c40 )) diff --git a/packages/graphql/pubspec.yaml b/packages/graphql/pubspec.yaml index 4a9361a8d..fe38f17bc 100644 --- a/packages/graphql/pubspec.yaml +++ b/packages/graphql/pubspec.yaml @@ -2,7 +2,7 @@ name: graphql description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.10 +version: 4.0.0-alpha.11 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql dependencies: meta: ^1.1.6 diff --git a/packages/graphql_flutter/CHANGELOG.md b/packages/graphql_flutter/CHANGELOG.md index 832439847..00dafbddb 100644 --- a/packages/graphql_flutter/CHANGELOG.md +++ b/packages/graphql_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +# 4.0.0-alpha.11 (2020-09-26) + +* **hotfix(client)**: `gql_http_link==0.3.2` for custom toJsons closing #734 ([98b8cf7](https://github.com/zino-app/graphql-flutter/commit/98b8cf771e9f982741d5041bd4a4f017ac46dc91)) + + # 4.0.0-alpha.10 (2020-09-26) * **hotfix(client)**: `gql_http_link==0.3.1` fix for bool json encoding support ([38b2fd2](https://github.com/zino-app/graphql-flutter/commit/38b2fd2abbb7e3439e35d139538816beb9e10c40 )) diff --git a/packages/graphql_flutter/pubspec.yaml b/packages/graphql_flutter/pubspec.yaml index df01c3b4e..607f12113 100644 --- a/packages/graphql_flutter/pubspec.yaml +++ b/packages/graphql_flutter/pubspec.yaml @@ -2,10 +2,10 @@ name: graphql_flutter description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package. -version: 4.0.0-alpha.10 +version: 4.0.0-alpha.11 homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter dependencies: - graphql: ^4.0.0-alpha.10 + graphql: ^4.0.0-alpha.11 #path: ../graphql gql_exec: ^0.2.2 flutter: From 950b292b249709d22c8fe32acf72ba733c24ef93 Mon Sep 17 00:00:00 2001 From: Michael Joseph Rosenthal Date: Sun, 27 Sep 2020 10:02:15 -0500 Subject: [PATCH 116/118] ref raw diagram --- packages/graphql/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/graphql/README.md b/packages/graphql/README.md index 6750fb36f..4e39c851f 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -425,7 +425,7 @@ as well as our own custom `WebSocketLink` and `AuthLink`. > **NB**: `WebSocketLink` and other "terminating links" must be used with `split` when there are multiple terminating links. The [`gql_link`] systm has a well-specified routing system: -![diagram](https://github.com/gql-dart/gql/blob/master/links/gql_link/assets/gql_link.svg) +![link diagram] a rundown of the composition api: @@ -566,6 +566,7 @@ final Link _link = _apqLink.concat(_httpLink); [discord-link]: https://discord.gg/tXTtBfC [gql-dart project]: https://github.com/gql-dart [`gql_link`]: https://github.com/gql-dart/gql/tree/master/links/gql_link +[link diagram]: https://raw.githubusercontent.com/gql-dart/gql/master/links/gql_link/assets/gql_link.svg [`gql`]: https://github.com/gql-dart/gql/tree/master/gql [`normalize`]: https://github.com/gql-dart/ferry/tree/master/normalize [apollo]: https://www.apollographql.com/ From b858a02505fc79dbba2df28262e4aa48c191ecf3 Mon Sep 17 00:00:00 2001 From: vasilich Date: Sun, 27 Sep 2020 22:15:31 +0300 Subject: [PATCH 117/118] v4 flutter bloc sample update --- .../flutter_bloc/ios/Flutter/Debug.xcconfig | 1 + .../flutter_bloc/ios/Flutter/Release.xcconfig | 1 + examples/flutter_bloc/ios/Podfile | 38 + .../ios/Runner.xcodeproj/project.pbxproj | 73 +- .../contents.xcworkspacedata | 3 + examples/flutter_bloc/lib/bloc.dart | 15 +- examples/flutter_bloc/lib/extended_bloc.dart | 161 ++-- .../lib/extended_bloc/graphql/bloc.dart | 133 ---- .../lib/extended_bloc/graphql/event.dart | 63 -- .../extended_bloc/graphql/event.freezed.dart | 648 ---------------- .../lib/extended_bloc/graphql/graphql.dart | 3 - .../lib/extended_bloc/graphql/state.dart | 70 -- .../extended_bloc/graphql/state.freezed.dart | 721 ------------------ .../lib/extended_bloc/repositories_bloc.dart | 22 +- examples/flutter_bloc/lib/hive_init.dart | 34 + examples/flutter_bloc/lib/main.dart | 6 +- examples/flutter_bloc/pubspec.yaml | 17 +- examples/flutter_bloc/test/bloc_test.dart | 4 + 18 files changed, 258 insertions(+), 1755 deletions(-) create mode 100644 examples/flutter_bloc/ios/Podfile delete mode 100644 examples/flutter_bloc/lib/extended_bloc/graphql/bloc.dart delete mode 100644 examples/flutter_bloc/lib/extended_bloc/graphql/event.dart delete mode 100644 examples/flutter_bloc/lib/extended_bloc/graphql/event.freezed.dart delete mode 100644 examples/flutter_bloc/lib/extended_bloc/graphql/graphql.dart delete mode 100644 examples/flutter_bloc/lib/extended_bloc/graphql/state.dart delete mode 100644 examples/flutter_bloc/lib/extended_bloc/graphql/state.freezed.dart create mode 100644 examples/flutter_bloc/lib/hive_init.dart diff --git a/examples/flutter_bloc/ios/Flutter/Debug.xcconfig b/examples/flutter_bloc/ios/Flutter/Debug.xcconfig index 592ceee85..e8efba114 100644 --- a/examples/flutter_bloc/ios/Flutter/Debug.xcconfig +++ b/examples/flutter_bloc/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/examples/flutter_bloc/ios/Flutter/Release.xcconfig b/examples/flutter_bloc/ios/Flutter/Release.xcconfig index 592ceee85..399e9340e 100644 --- a/examples/flutter_bloc/ios/Flutter/Release.xcconfig +++ b/examples/flutter_bloc/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/examples/flutter_bloc/ios/Podfile b/examples/flutter_bloc/ios/Podfile new file mode 100644 index 000000000..f7d6a5e68 --- /dev/null +++ b/examples/flutter_bloc/ios/Podfile @@ -0,0 +1,38 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/examples/flutter_bloc/ios/Runner.xcodeproj/project.pbxproj b/examples/flutter_bloc/ios/Runner.xcodeproj/project.pbxproj index c6f27b217..3c3aed5c1 100644 --- a/examples/flutter_bloc/ios/Runner.xcodeproj/project.pbxproj +++ b/examples/flutter_bloc/ios/Runner.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 5CA22CE3629E2B7CB9BEEEFE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18DF0D89439134222F2FF830 /* libPods-Runner.a */; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; @@ -33,7 +34,11 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 18DF0D89439134222F2FF830 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 40996C314C6E9E0B16D16A81 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 58CBAC75E2382AB59FB2BDD3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 6933C3904819825D210B7737 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -52,12 +57,32 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5CA22CE3629E2B7CB9BEEEFE /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 34BDF72114FCEEFDE8EE0D84 /* Pods */ = { + isa = PBXGroup; + children = ( + 6933C3904819825D210B7737 /* Pods-Runner.debug.xcconfig */, + 40996C314C6E9E0B16D16A81 /* Pods-Runner.release.xcconfig */, + 58CBAC75E2382AB59FB2BDD3 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 72D400AF5A9CD38CB2CFC008 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 18DF0D89439134222F2FF830 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -75,7 +100,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + 34BDF72114FCEEFDE8EE0D84 /* Pods */, + 72D400AF5A9CD38CB2CFC008 /* Frameworks */, ); sourceTree = ""; }; @@ -118,12 +144,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 5BD8D1480982C5095378BE4D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E0273651DDFDE1C239C56D6C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -196,6 +224,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 5BD8D1480982C5095378BE4D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -210,6 +260,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + E0273651DDFDE1C239C56D6C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${PODS_ROOT}/../Flutter/Flutter.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -247,7 +315,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -321,7 +388,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -377,7 +443,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/examples/flutter_bloc/ios/Runner.xcworkspace/contents.xcworkspacedata b/examples/flutter_bloc/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16e..21a3cc14c 100644 --- a/examples/flutter_bloc/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/examples/flutter_bloc/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/examples/flutter_bloc/lib/bloc.dart b/examples/flutter_bloc/lib/bloc.dart index ec91d2faf..a69e62c2c 100644 --- a/examples/flutter_bloc/lib/bloc.dart +++ b/examples/flutter_bloc/lib/bloc.dart @@ -12,13 +12,20 @@ class BlocPage extends StatefulWidget { } class _BlocPageState extends State { + MyGithubReposBloc bloc; @override void initState() { super.initState(); - final bloc = BlocProvider.of(context); + bloc = BlocProvider.of(context); bloc.add(LoadMyRepos(numOfReposToLoad: 50)); } + @override + void dispose() { + bloc.close(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -38,9 +45,7 @@ class _BlocPageState extends State { keyboardType: TextInputType.number, textAlign: TextAlign.center, onChanged: (String n) { - final reposBloc = BlocProvider.of(context); - reposBloc - .add(LoadMyRepos(numOfReposToLoad: int.parse(n) ?? 50)); + bloc.add(LoadMyRepos(numOfReposToLoad: int.parse(n) ?? 50)); }, ), SizedBox( @@ -64,7 +69,7 @@ class LoadRepositories extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - bloc: bloc, + cubit: bloc, builder: (BuildContext context, MyGithubReposState state) { if (state is ReposLoading) { return Expanded( diff --git a/examples/flutter_bloc/lib/extended_bloc.dart b/examples/flutter_bloc/lib/extended_bloc.dart index f31bc44f2..7e850fb07 100644 --- a/examples/flutter_bloc/lib/extended_bloc.dart +++ b/examples/flutter_bloc/lib/extended_bloc.dart @@ -3,10 +3,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:graphql/client.dart'; +import 'package:graphql_flutter_bloc/graphql_flutter_bloc.dart'; import 'package:graphql_flutter_bloc_example/extended_bloc/repositories_bloc.dart'; -import 'package:graphql_flutter_bloc_example/extended_bloc/graphql/event.dart'; -import 'package:graphql_flutter_bloc_example/extended_bloc/graphql/state.dart'; class ExtendedBloc extends StatefulWidget { @override @@ -24,8 +23,8 @@ class _ExtendedBlocState extends State { bloc = BlocProvider.of(context)..run(); } - Future _handleRefreshStart(Bloc bloc) { - bloc.add(GraphqlRefetchEvent>()); + Future _handleRefreshStart(RepositoriesBloc bloc) { + bloc.refetch(); return _refreshCompleter.future; } @@ -40,6 +39,58 @@ class _ExtendedBlocState extends State { _refreshCompleter = Completer(); } + Widget _displayResults(Map data, QueryResult result) { + final itemCount = data['viewer']['repositories']['nodes'].length; + + if (itemCount == 0) { + return ListView(children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.inbox), + SizedBox(width: 8), + Text('No data'), + ], + ) + ]); + } else { + return ListView.separated( + separatorBuilder: (_, __) => SizedBox( + height: 8.0, + ), + key: PageStorageKey('reports'), + itemCount: itemCount, + itemBuilder: (BuildContext context, int index) { + final pageInfo = data['viewer']['repositories']['pageInfo']; + + if (bloc.shouldFetchMore(index, 1)) { + bloc.fetchMore(after: pageInfo['endCursor']); + } + + final node = data['viewer']['repositories']['nodes'][index]; + + Widget tile = ListTile( + title: Text(node['name']), + ); + + if (bloc.isFetchingMore && index == itemCount - 1) { + tile = Column( + children: [ + tile, + Padding( + padding: const EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ], + ); + } + + return tile; + }, + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -48,87 +99,27 @@ class _ExtendedBlocState extends State { ), body: RefreshIndicator( onRefresh: () async => _handleRefreshStart(bloc), - child: - BlocBuilder>>( - bloc: bloc, - builder: (_, state) { - Widget child = Container(); - - if (bloc.isLoading) { - child = Center(child: CircularProgressIndicator()); - } - - if (bloc.hasError) { - _handleRefreshEnd(); - child = ListView(children: [ - Text( - bloc.getError, - style: TextStyle(color: Theme.of(context).errorColor), - ) - ]); - } - - if (bloc.hasData) { - _handleRefreshEnd(); - final itemCount = - state.data['viewer']['repositories']['nodes'].length; - - if (itemCount == 0) { - child = ListView(children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.inbox), - SizedBox(width: 8), - Text('No data'), - ], - ) - ]); - } else { - child = ListView.separated( - separatorBuilder: (_, __) => SizedBox( - height: 8.0, - ), - key: PageStorageKey('reports'), - itemCount: itemCount, - itemBuilder: (BuildContext context, int index) { - final pageInfo = - state.data['viewer']['repositories']['pageInfo']; - - if (bloc.shouldFetchMore(index, 1)) { - bloc.fetchMore(after: pageInfo['endCursor']); - } - - final node = state.data['viewer']['repositories'] - ['nodes'][index]; - - Widget tile = ListTile( - title: Text(node['name']), - ); - - if (bloc.isFetchingMore && index == itemCount - 1) { - tile = Column( - children: [ - tile, - Padding( - padding: const EdgeInsets.all(16.0), - child: CircularProgressIndicator(), - ), - ], - ); - } - - return tile; - }, - ); - } - } - - return AnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: child, - ); - }), + child: BlocBuilder>>( + cubit: bloc, + builder: (_, state) { + if (state is! QueryStateRefetch) { + _handleRefreshEnd(); + } + + return state.when( + initial: () => Container(), + loading: (_) => Center(child: CircularProgressIndicator()), + error: (_, __) => ListView(children: [ + Text( + bloc.getError, + style: TextStyle(color: Theme.of(context).errorColor), + ) + ]), + loaded: _displayResults, + refetch: _displayResults, + fetchMore: _displayResults, + ); + }), ), ); } diff --git a/examples/flutter_bloc/lib/extended_bloc/graphql/bloc.dart b/examples/flutter_bloc/lib/extended_bloc/graphql/bloc.dart deleted file mode 100644 index 0d3aa6545..000000000 --- a/examples/flutter_bloc/lib/extended_bloc/graphql/bloc.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'dart:async'; -import 'package:bloc/bloc.dart'; -import 'package:graphql/client.dart'; -import 'package:meta/meta.dart'; - -import 'event.dart'; -import 'state.dart'; - -abstract class GraphqlBloc extends Bloc, GraphqlState> { - GraphQLClient client; - ObservableQuery result; - WatchQueryOptions options; - - GraphqlBloc({@required this.client, @required this.options}) - : super(GraphqlInitialState()) { - result = client.watchQuery(options); - - result.stream.listen((QueryResult result) { - if (state is GraphqlRefetchState && - result.source == QueryResultSource.cache) { - return; - } - - if (result.isLoading && result.data == null) { - add(GraphqlLoadingEvent(result: result)); - } - - if (!result.isLoading && result.data != null) { - add( - GraphqlLoadedEvent( - data: parseData(result.data), - result: result, - ), - ); - } - - if (result.hasException) { - add(GraphqlErrorEvent(error: result.exception, result: result)); - } - }); - } - - void dispose() { - result.close(); - } - - void run() { - add(GraphqlRunQueryEvent()); - } - - void refetch() { - add(GraphqlRefetchEvent()); - } - - bool shouldFetchMore(int i, int threshold) => false; - - bool get isFetchingMore => state is GraphqlFetchMoreState; - - bool get isLoading => state is GraphqlLoadingState; - - bool get isRefetching => state is GraphqlRefetchState; - - T parseData(Map data); - - bool get hasData => (state is GraphqlLoadedState || - state is GraphqlFetchMoreState || - state is GraphqlRefetchState); - - bool get hasError => state is GraphqlErrorState; - - String get getError => hasError - ? parseOperationException((state as GraphqlErrorState).error) - : null; - - Future _runQuery() async { - result.fetchResults(); - } - - void _fetchMore(FetchMoreOptions options) { - result.fetchMore(options); - } - - void _refetch() => result.refetch(); - - @override - Stream> mapEventToState(GraphqlEvent event) async* { - if (event is GraphqlRunQueryEvent) { - _runQuery(); - } - - if (event is GraphqlLoadingEvent) { - yield GraphqlLoadingState(result: event.result); - } - - if (event is GraphqlLoadedEvent) { - yield GraphqlLoadedState(data: event.data, result: event.result); - } - - if (event is GraphqlErrorEvent) { - yield GraphqlErrorState(error: event.error, result: event.result); - } - - if (event is GraphqlRefetchEvent) { - yield GraphqlRefetchState(data: state.data, result: null); - _refetch(); - } - - if (state is GraphqlLoadedState && event is GraphqlFetchMoreEvent) { - yield GraphqlFetchMoreState(data: state.data, result: null); - _fetchMore(event.options); - } - } -} - -String parseOperationException(OperationException error) { - if (error.linkException != null) { - final exception = error.linkException; - - if (exception is NetworkException) { - return 'Failed to connect to ${exception.uri}'; - } else { - return exception.toString(); - } - } - - if (error.graphqlErrors != null && error.graphqlErrors.isNotEmpty) { - final errors = error.graphqlErrors; - - return errors.first.message; - } - - return 'Unknown error'; -} diff --git a/examples/flutter_bloc/lib/extended_bloc/graphql/event.dart b/examples/flutter_bloc/lib/extended_bloc/graphql/event.dart deleted file mode 100644 index e799a5c9a..000000000 --- a/examples/flutter_bloc/lib/extended_bloc/graphql/event.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:graphql/client.dart'; -import 'package:meta/meta.dart'; - -part 'event.freezed.dart'; - -abstract class GraphqlEvent {} - -@freezed -abstract class GraphqlErrorEvent extends GraphqlEvent - implements _$GraphqlErrorEvent { - GraphqlErrorEvent._(); - - factory GraphqlErrorEvent({ - @required OperationException error, - @required QueryResult result, - }) = _GraphqlErrorEvent; -} - -@freezed -abstract class GraphqlRunQueryEvent extends GraphqlEvent - implements _$GraphqlRunQueryEvent { - GraphqlRunQueryEvent._(); - - factory GraphqlRunQueryEvent() = _GraphqlRunQueryEvent; -} - -@freezed -abstract class GraphqlLoadingEvent extends GraphqlEvent - implements _$GraphqlLoadingEvent { - GraphqlLoadingEvent._(); - - factory GraphqlLoadingEvent({ - @required QueryResult result, - }) = _GraphqlLoadingEvent; -} - -@freezed -abstract class GraphqlLoadedEvent extends GraphqlEvent - implements _$GraphqlLoadedEvent { - GraphqlLoadedEvent._(); - - factory GraphqlLoadedEvent({@required T data, @required QueryResult result}) = - _GraphqlLoadedEvent; -} - -@freezed -abstract class GraphqlRefetchEvent extends GraphqlEvent - implements _$GraphqlRefetchEvent { - GraphqlRefetchEvent._(); - - factory GraphqlRefetchEvent() = _GraphqlRefetchEvent; -} - -@freezed -abstract class GraphqlFetchMoreEvent extends GraphqlEvent - implements _$GraphqlFetchMoreEvent { - GraphqlFetchMoreEvent._(); - - factory GraphqlFetchMoreEvent({ - @required FetchMoreOptions options, - }) = _GraphqlFetchMoreEvent; -} diff --git a/examples/flutter_bloc/lib/extended_bloc/graphql/event.freezed.dart b/examples/flutter_bloc/lib/extended_bloc/graphql/event.freezed.dart deleted file mode 100644 index 80ff660a9..000000000 --- a/examples/flutter_bloc/lib/extended_bloc/graphql/event.freezed.dart +++ /dev/null @@ -1,648 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies - -part of 'event.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -class _$GraphqlErrorEventTearOff { - const _$GraphqlErrorEventTearOff(); - -// ignore: unused_element - _GraphqlErrorEvent call( - {@required OperationException error, @required QueryResult result}) { - return _GraphqlErrorEvent( - error: error, - result: result, - ); - } -} - -// ignore: unused_element -const $GraphqlErrorEvent = _$GraphqlErrorEventTearOff(); - -mixin _$GraphqlErrorEvent { - OperationException get error; - QueryResult get result; - - $GraphqlErrorEventCopyWith> get copyWith; -} - -abstract class $GraphqlErrorEventCopyWith { - factory $GraphqlErrorEventCopyWith(GraphqlErrorEvent value, - $Res Function(GraphqlErrorEvent) then) = - _$GraphqlErrorEventCopyWithImpl; - $Res call({OperationException error, QueryResult result}); -} - -class _$GraphqlErrorEventCopyWithImpl - implements $GraphqlErrorEventCopyWith { - _$GraphqlErrorEventCopyWithImpl(this._value, this._then); - - final GraphqlErrorEvent _value; - // ignore: unused_field - final $Res Function(GraphqlErrorEvent) _then; - - @override - $Res call({ - Object error = freezed, - Object result = freezed, - }) { - return _then(_value.copyWith( - error: error == freezed ? _value.error : error as OperationException, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -abstract class _$GraphqlErrorEventCopyWith - implements $GraphqlErrorEventCopyWith { - factory _$GraphqlErrorEventCopyWith(_GraphqlErrorEvent value, - $Res Function(_GraphqlErrorEvent) then) = - __$GraphqlErrorEventCopyWithImpl; - @override - $Res call({OperationException error, QueryResult result}); -} - -class __$GraphqlErrorEventCopyWithImpl - extends _$GraphqlErrorEventCopyWithImpl - implements _$GraphqlErrorEventCopyWith { - __$GraphqlErrorEventCopyWithImpl( - _GraphqlErrorEvent _value, $Res Function(_GraphqlErrorEvent) _then) - : super(_value, (v) => _then(v as _GraphqlErrorEvent)); - - @override - _GraphqlErrorEvent get _value => super._value as _GraphqlErrorEvent; - - @override - $Res call({ - Object error = freezed, - Object result = freezed, - }) { - return _then(_GraphqlErrorEvent( - error: error == freezed ? _value.error : error as OperationException, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -class _$_GraphqlErrorEvent extends _GraphqlErrorEvent { - _$_GraphqlErrorEvent({@required this.error, @required this.result}) - : assert(error != null), - assert(result != null), - super._(); - - @override - final OperationException error; - @override - final QueryResult result; - - @override - String toString() { - return 'GraphqlErrorEvent<$T>(error: $error, result: $result)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other is _GraphqlErrorEvent && - (identical(other.error, error) || - const DeepCollectionEquality().equals(other.error, error)) && - (identical(other.result, result) || - const DeepCollectionEquality().equals(other.result, result))); - } - - @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(error) ^ - const DeepCollectionEquality().hash(result); - - @override - _$GraphqlErrorEventCopyWith> get copyWith => - __$GraphqlErrorEventCopyWithImpl>( - this, _$identity); -} - -abstract class _GraphqlErrorEvent extends GraphqlErrorEvent { - _GraphqlErrorEvent._() : super._(); - factory _GraphqlErrorEvent( - {@required OperationException error, - @required QueryResult result}) = _$_GraphqlErrorEvent; - - @override - OperationException get error; - @override - QueryResult get result; - @override - _$GraphqlErrorEventCopyWith> get copyWith; -} - -class _$GraphqlRunQueryEventTearOff { - const _$GraphqlRunQueryEventTearOff(); - -// ignore: unused_element - _GraphqlRunQueryEvent call() { - return _GraphqlRunQueryEvent(); - } -} - -// ignore: unused_element -const $GraphqlRunQueryEvent = _$GraphqlRunQueryEventTearOff(); - -mixin _$GraphqlRunQueryEvent {} - -abstract class $GraphqlRunQueryEventCopyWith { - factory $GraphqlRunQueryEventCopyWith(GraphqlRunQueryEvent value, - $Res Function(GraphqlRunQueryEvent) then) = - _$GraphqlRunQueryEventCopyWithImpl; -} - -class _$GraphqlRunQueryEventCopyWithImpl - implements $GraphqlRunQueryEventCopyWith { - _$GraphqlRunQueryEventCopyWithImpl(this._value, this._then); - - final GraphqlRunQueryEvent _value; - // ignore: unused_field - final $Res Function(GraphqlRunQueryEvent) _then; -} - -abstract class _$GraphqlRunQueryEventCopyWith { - factory _$GraphqlRunQueryEventCopyWith(_GraphqlRunQueryEvent value, - $Res Function(_GraphqlRunQueryEvent) then) = - __$GraphqlRunQueryEventCopyWithImpl; -} - -class __$GraphqlRunQueryEventCopyWithImpl - extends _$GraphqlRunQueryEventCopyWithImpl - implements _$GraphqlRunQueryEventCopyWith { - __$GraphqlRunQueryEventCopyWithImpl(_GraphqlRunQueryEvent _value, - $Res Function(_GraphqlRunQueryEvent) _then) - : super(_value, (v) => _then(v as _GraphqlRunQueryEvent)); - - @override - _GraphqlRunQueryEvent get _value => - super._value as _GraphqlRunQueryEvent; -} - -class _$_GraphqlRunQueryEvent extends _GraphqlRunQueryEvent { - _$_GraphqlRunQueryEvent() : super._(); - - @override - String toString() { - return 'GraphqlRunQueryEvent<$T>()'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || (other is _GraphqlRunQueryEvent); - } - - @override - int get hashCode => runtimeType.hashCode; -} - -abstract class _GraphqlRunQueryEvent extends GraphqlRunQueryEvent { - _GraphqlRunQueryEvent._() : super._(); - factory _GraphqlRunQueryEvent() = _$_GraphqlRunQueryEvent; -} - -class _$GraphqlLoadingEventTearOff { - const _$GraphqlLoadingEventTearOff(); - -// ignore: unused_element - _GraphqlLoadingEvent call({@required QueryResult result}) { - return _GraphqlLoadingEvent( - result: result, - ); - } -} - -// ignore: unused_element -const $GraphqlLoadingEvent = _$GraphqlLoadingEventTearOff(); - -mixin _$GraphqlLoadingEvent { - QueryResult get result; - - $GraphqlLoadingEventCopyWith> get copyWith; -} - -abstract class $GraphqlLoadingEventCopyWith { - factory $GraphqlLoadingEventCopyWith(GraphqlLoadingEvent value, - $Res Function(GraphqlLoadingEvent) then) = - _$GraphqlLoadingEventCopyWithImpl; - $Res call({QueryResult result}); -} - -class _$GraphqlLoadingEventCopyWithImpl - implements $GraphqlLoadingEventCopyWith { - _$GraphqlLoadingEventCopyWithImpl(this._value, this._then); - - final GraphqlLoadingEvent _value; - // ignore: unused_field - final $Res Function(GraphqlLoadingEvent) _then; - - @override - $Res call({ - Object result = freezed, - }) { - return _then(_value.copyWith( - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -abstract class _$GraphqlLoadingEventCopyWith - implements $GraphqlLoadingEventCopyWith { - factory _$GraphqlLoadingEventCopyWith(_GraphqlLoadingEvent value, - $Res Function(_GraphqlLoadingEvent) then) = - __$GraphqlLoadingEventCopyWithImpl; - @override - $Res call({QueryResult result}); -} - -class __$GraphqlLoadingEventCopyWithImpl - extends _$GraphqlLoadingEventCopyWithImpl - implements _$GraphqlLoadingEventCopyWith { - __$GraphqlLoadingEventCopyWithImpl(_GraphqlLoadingEvent _value, - $Res Function(_GraphqlLoadingEvent) _then) - : super(_value, (v) => _then(v as _GraphqlLoadingEvent)); - - @override - _GraphqlLoadingEvent get _value => super._value as _GraphqlLoadingEvent; - - @override - $Res call({ - Object result = freezed, - }) { - return _then(_GraphqlLoadingEvent( - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -class _$_GraphqlLoadingEvent extends _GraphqlLoadingEvent { - _$_GraphqlLoadingEvent({@required this.result}) - : assert(result != null), - super._(); - - @override - final QueryResult result; - - @override - String toString() { - return 'GraphqlLoadingEvent<$T>(result: $result)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other is _GraphqlLoadingEvent && - (identical(other.result, result) || - const DeepCollectionEquality().equals(other.result, result))); - } - - @override - int get hashCode => - runtimeType.hashCode ^ const DeepCollectionEquality().hash(result); - - @override - _$GraphqlLoadingEventCopyWith> get copyWith => - __$GraphqlLoadingEventCopyWithImpl>( - this, _$identity); -} - -abstract class _GraphqlLoadingEvent extends GraphqlLoadingEvent { - _GraphqlLoadingEvent._() : super._(); - factory _GraphqlLoadingEvent({@required QueryResult result}) = - _$_GraphqlLoadingEvent; - - @override - QueryResult get result; - @override - _$GraphqlLoadingEventCopyWith> get copyWith; -} - -class _$GraphqlLoadedEventTearOff { - const _$GraphqlLoadedEventTearOff(); - -// ignore: unused_element - _GraphqlLoadedEvent call( - {@required T data, @required QueryResult result}) { - return _GraphqlLoadedEvent( - data: data, - result: result, - ); - } -} - -// ignore: unused_element -const $GraphqlLoadedEvent = _$GraphqlLoadedEventTearOff(); - -mixin _$GraphqlLoadedEvent { - T get data; - QueryResult get result; - - $GraphqlLoadedEventCopyWith> get copyWith; -} - -abstract class $GraphqlLoadedEventCopyWith { - factory $GraphqlLoadedEventCopyWith(GraphqlLoadedEvent value, - $Res Function(GraphqlLoadedEvent) then) = - _$GraphqlLoadedEventCopyWithImpl; - $Res call({T data, QueryResult result}); -} - -class _$GraphqlLoadedEventCopyWithImpl - implements $GraphqlLoadedEventCopyWith { - _$GraphqlLoadedEventCopyWithImpl(this._value, this._then); - - final GraphqlLoadedEvent _value; - // ignore: unused_field - final $Res Function(GraphqlLoadedEvent) _then; - - @override - $Res call({ - Object data = freezed, - Object result = freezed, - }) { - return _then(_value.copyWith( - data: data == freezed ? _value.data : data as T, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -abstract class _$GraphqlLoadedEventCopyWith - implements $GraphqlLoadedEventCopyWith { - factory _$GraphqlLoadedEventCopyWith(_GraphqlLoadedEvent value, - $Res Function(_GraphqlLoadedEvent) then) = - __$GraphqlLoadedEventCopyWithImpl; - @override - $Res call({T data, QueryResult result}); -} - -class __$GraphqlLoadedEventCopyWithImpl - extends _$GraphqlLoadedEventCopyWithImpl - implements _$GraphqlLoadedEventCopyWith { - __$GraphqlLoadedEventCopyWithImpl(_GraphqlLoadedEvent _value, - $Res Function(_GraphqlLoadedEvent) _then) - : super(_value, (v) => _then(v as _GraphqlLoadedEvent)); - - @override - _GraphqlLoadedEvent get _value => super._value as _GraphqlLoadedEvent; - - @override - $Res call({ - Object data = freezed, - Object result = freezed, - }) { - return _then(_GraphqlLoadedEvent( - data: data == freezed ? _value.data : data as T, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -class _$_GraphqlLoadedEvent extends _GraphqlLoadedEvent { - _$_GraphqlLoadedEvent({@required this.data, @required this.result}) - : assert(data != null), - assert(result != null), - super._(); - - @override - final T data; - @override - final QueryResult result; - - @override - String toString() { - return 'GraphqlLoadedEvent<$T>(data: $data, result: $result)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other is _GraphqlLoadedEvent && - (identical(other.data, data) || - const DeepCollectionEquality().equals(other.data, data)) && - (identical(other.result, result) || - const DeepCollectionEquality().equals(other.result, result))); - } - - @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(data) ^ - const DeepCollectionEquality().hash(result); - - @override - _$GraphqlLoadedEventCopyWith> get copyWith => - __$GraphqlLoadedEventCopyWithImpl>( - this, _$identity); -} - -abstract class _GraphqlLoadedEvent extends GraphqlLoadedEvent { - _GraphqlLoadedEvent._() : super._(); - factory _GraphqlLoadedEvent( - {@required T data, - @required QueryResult result}) = _$_GraphqlLoadedEvent; - - @override - T get data; - @override - QueryResult get result; - @override - _$GraphqlLoadedEventCopyWith> get copyWith; -} - -class _$GraphqlRefetchEventTearOff { - const _$GraphqlRefetchEventTearOff(); - -// ignore: unused_element - _GraphqlRefetchEvent call() { - return _GraphqlRefetchEvent(); - } -} - -// ignore: unused_element -const $GraphqlRefetchEvent = _$GraphqlRefetchEventTearOff(); - -mixin _$GraphqlRefetchEvent {} - -abstract class $GraphqlRefetchEventCopyWith { - factory $GraphqlRefetchEventCopyWith(GraphqlRefetchEvent value, - $Res Function(GraphqlRefetchEvent) then) = - _$GraphqlRefetchEventCopyWithImpl; -} - -class _$GraphqlRefetchEventCopyWithImpl - implements $GraphqlRefetchEventCopyWith { - _$GraphqlRefetchEventCopyWithImpl(this._value, this._then); - - final GraphqlRefetchEvent _value; - // ignore: unused_field - final $Res Function(GraphqlRefetchEvent) _then; -} - -abstract class _$GraphqlRefetchEventCopyWith { - factory _$GraphqlRefetchEventCopyWith(_GraphqlRefetchEvent value, - $Res Function(_GraphqlRefetchEvent) then) = - __$GraphqlRefetchEventCopyWithImpl; -} - -class __$GraphqlRefetchEventCopyWithImpl - extends _$GraphqlRefetchEventCopyWithImpl - implements _$GraphqlRefetchEventCopyWith { - __$GraphqlRefetchEventCopyWithImpl(_GraphqlRefetchEvent _value, - $Res Function(_GraphqlRefetchEvent) _then) - : super(_value, (v) => _then(v as _GraphqlRefetchEvent)); - - @override - _GraphqlRefetchEvent get _value => super._value as _GraphqlRefetchEvent; -} - -class _$_GraphqlRefetchEvent extends _GraphqlRefetchEvent { - _$_GraphqlRefetchEvent() : super._(); - - @override - String toString() { - return 'GraphqlRefetchEvent<$T>()'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || (other is _GraphqlRefetchEvent); - } - - @override - int get hashCode => runtimeType.hashCode; -} - -abstract class _GraphqlRefetchEvent extends GraphqlRefetchEvent { - _GraphqlRefetchEvent._() : super._(); - factory _GraphqlRefetchEvent() = _$_GraphqlRefetchEvent; -} - -class _$GraphqlFetchMoreEventTearOff { - const _$GraphqlFetchMoreEventTearOff(); - -// ignore: unused_element - _GraphqlFetchMoreEvent call({@required FetchMoreOptions options}) { - return _GraphqlFetchMoreEvent( - options: options, - ); - } -} - -// ignore: unused_element -const $GraphqlFetchMoreEvent = _$GraphqlFetchMoreEventTearOff(); - -mixin _$GraphqlFetchMoreEvent { - FetchMoreOptions get options; - - $GraphqlFetchMoreEventCopyWith> get copyWith; -} - -abstract class $GraphqlFetchMoreEventCopyWith { - factory $GraphqlFetchMoreEventCopyWith(GraphqlFetchMoreEvent value, - $Res Function(GraphqlFetchMoreEvent) then) = - _$GraphqlFetchMoreEventCopyWithImpl; - $Res call({FetchMoreOptions options}); -} - -class _$GraphqlFetchMoreEventCopyWithImpl - implements $GraphqlFetchMoreEventCopyWith { - _$GraphqlFetchMoreEventCopyWithImpl(this._value, this._then); - - final GraphqlFetchMoreEvent _value; - // ignore: unused_field - final $Res Function(GraphqlFetchMoreEvent) _then; - - @override - $Res call({ - Object options = freezed, - }) { - return _then(_value.copyWith( - options: - options == freezed ? _value.options : options as FetchMoreOptions, - )); - } -} - -abstract class _$GraphqlFetchMoreEventCopyWith - implements $GraphqlFetchMoreEventCopyWith { - factory _$GraphqlFetchMoreEventCopyWith(_GraphqlFetchMoreEvent value, - $Res Function(_GraphqlFetchMoreEvent) then) = - __$GraphqlFetchMoreEventCopyWithImpl; - @override - $Res call({FetchMoreOptions options}); -} - -class __$GraphqlFetchMoreEventCopyWithImpl - extends _$GraphqlFetchMoreEventCopyWithImpl - implements _$GraphqlFetchMoreEventCopyWith { - __$GraphqlFetchMoreEventCopyWithImpl(_GraphqlFetchMoreEvent _value, - $Res Function(_GraphqlFetchMoreEvent) _then) - : super(_value, (v) => _then(v as _GraphqlFetchMoreEvent)); - - @override - _GraphqlFetchMoreEvent get _value => - super._value as _GraphqlFetchMoreEvent; - - @override - $Res call({ - Object options = freezed, - }) { - return _then(_GraphqlFetchMoreEvent( - options: - options == freezed ? _value.options : options as FetchMoreOptions, - )); - } -} - -class _$_GraphqlFetchMoreEvent extends _GraphqlFetchMoreEvent { - _$_GraphqlFetchMoreEvent({@required this.options}) - : assert(options != null), - super._(); - - @override - final FetchMoreOptions options; - - @override - String toString() { - return 'GraphqlFetchMoreEvent<$T>(options: $options)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other is _GraphqlFetchMoreEvent && - (identical(other.options, options) || - const DeepCollectionEquality().equals(other.options, options))); - } - - @override - int get hashCode => - runtimeType.hashCode ^ const DeepCollectionEquality().hash(options); - - @override - _$GraphqlFetchMoreEventCopyWith> get copyWith => - __$GraphqlFetchMoreEventCopyWithImpl>( - this, _$identity); -} - -abstract class _GraphqlFetchMoreEvent extends GraphqlFetchMoreEvent { - _GraphqlFetchMoreEvent._() : super._(); - factory _GraphqlFetchMoreEvent({@required FetchMoreOptions options}) = - _$_GraphqlFetchMoreEvent; - - @override - FetchMoreOptions get options; - @override - _$GraphqlFetchMoreEventCopyWith> get copyWith; -} diff --git a/examples/flutter_bloc/lib/extended_bloc/graphql/graphql.dart b/examples/flutter_bloc/lib/extended_bloc/graphql/graphql.dart deleted file mode 100644 index c73b25cb2..000000000 --- a/examples/flutter_bloc/lib/extended_bloc/graphql/graphql.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'bloc.dart'; -export 'state.dart'; -export 'event.dart'; diff --git a/examples/flutter_bloc/lib/extended_bloc/graphql/state.dart b/examples/flutter_bloc/lib/extended_bloc/graphql/state.dart deleted file mode 100644 index 94e192ce0..000000000 --- a/examples/flutter_bloc/lib/extended_bloc/graphql/state.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:graphql/client.dart'; -import 'package:meta/meta.dart'; - -part 'state.freezed.dart'; - -abstract class GraphqlState { - final T data; - - const GraphqlState({@required this.data}); -} - -@freezed -abstract class GraphqlInitialState extends GraphqlState - implements _$GraphqlInitialState { - GraphqlInitialState._(); - - factory GraphqlInitialState() = _GraphqlInitialState; -} - -@freezed -abstract class GraphqlLoadingState extends GraphqlState - implements _$GraphqlLoadingState { - GraphqlLoadingState._(); - - factory GraphqlLoadingState({ - @required QueryResult result, - }) = _GraphqlLoadingState; -} - -@freezed -abstract class GraphqlErrorState extends GraphqlState - implements _$GraphqlErrorState { - GraphqlErrorState._(); - - factory GraphqlErrorState( - {@required OperationException error, - @required QueryResult result}) = _GraphqlErrorState; -} - -@freezed -abstract class GraphqlLoadedState extends GraphqlState - implements _$GraphqlLoadedState { - GraphqlLoadedState._(); - - factory GraphqlLoadedState({@required T data, @required QueryResult result}) = - _GraphqlLoadedState; -} - -@freezed -abstract class GraphqlRefetchState extends GraphqlState - implements _$GraphqlRefetchState { - GraphqlRefetchState._(); - - factory GraphqlRefetchState({ - @required T data, - QueryResult result, - }) = _GraphqlRefetchState; -} - -@freezed -abstract class GraphqlFetchMoreState extends GraphqlState - implements _$GraphqlFetchMoreState { - GraphqlFetchMoreState._(); - - factory GraphqlFetchMoreState({ - @required T data, - QueryResult result, - }) = _GraphqlFetchMoreState; -} diff --git a/examples/flutter_bloc/lib/extended_bloc/graphql/state.freezed.dart b/examples/flutter_bloc/lib/extended_bloc/graphql/state.freezed.dart deleted file mode 100644 index 91ed8a51f..000000000 --- a/examples/flutter_bloc/lib/extended_bloc/graphql/state.freezed.dart +++ /dev/null @@ -1,721 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies - -part of 'state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -class _$GraphqlInitialStateTearOff { - const _$GraphqlInitialStateTearOff(); - -// ignore: unused_element - _GraphqlInitialState call() { - return _GraphqlInitialState(); - } -} - -// ignore: unused_element -const $GraphqlInitialState = _$GraphqlInitialStateTearOff(); - -mixin _$GraphqlInitialState {} - -abstract class $GraphqlInitialStateCopyWith { - factory $GraphqlInitialStateCopyWith(GraphqlInitialState value, - $Res Function(GraphqlInitialState) then) = - _$GraphqlInitialStateCopyWithImpl; -} - -class _$GraphqlInitialStateCopyWithImpl - implements $GraphqlInitialStateCopyWith { - _$GraphqlInitialStateCopyWithImpl(this._value, this._then); - - final GraphqlInitialState _value; - // ignore: unused_field - final $Res Function(GraphqlInitialState) _then; -} - -abstract class _$GraphqlInitialStateCopyWith { - factory _$GraphqlInitialStateCopyWith(_GraphqlInitialState value, - $Res Function(_GraphqlInitialState) then) = - __$GraphqlInitialStateCopyWithImpl; -} - -class __$GraphqlInitialStateCopyWithImpl - extends _$GraphqlInitialStateCopyWithImpl - implements _$GraphqlInitialStateCopyWith { - __$GraphqlInitialStateCopyWithImpl(_GraphqlInitialState _value, - $Res Function(_GraphqlInitialState) _then) - : super(_value, (v) => _then(v as _GraphqlInitialState)); - - @override - _GraphqlInitialState get _value => super._value as _GraphqlInitialState; -} - -class _$_GraphqlInitialState extends _GraphqlInitialState { - _$_GraphqlInitialState() : super._(); - - @override - String toString() { - return 'GraphqlInitialState<$T>()'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || (other is _GraphqlInitialState); - } - - @override - int get hashCode => runtimeType.hashCode; -} - -abstract class _GraphqlInitialState extends GraphqlInitialState { - _GraphqlInitialState._() : super._(); - factory _GraphqlInitialState() = _$_GraphqlInitialState; -} - -class _$GraphqlLoadingStateTearOff { - const _$GraphqlLoadingStateTearOff(); - -// ignore: unused_element - _GraphqlLoadingState call({@required QueryResult result}) { - return _GraphqlLoadingState( - result: result, - ); - } -} - -// ignore: unused_element -const $GraphqlLoadingState = _$GraphqlLoadingStateTearOff(); - -mixin _$GraphqlLoadingState { - QueryResult get result; - - $GraphqlLoadingStateCopyWith> get copyWith; -} - -abstract class $GraphqlLoadingStateCopyWith { - factory $GraphqlLoadingStateCopyWith(GraphqlLoadingState value, - $Res Function(GraphqlLoadingState) then) = - _$GraphqlLoadingStateCopyWithImpl; - $Res call({QueryResult result}); -} - -class _$GraphqlLoadingStateCopyWithImpl - implements $GraphqlLoadingStateCopyWith { - _$GraphqlLoadingStateCopyWithImpl(this._value, this._then); - - final GraphqlLoadingState _value; - // ignore: unused_field - final $Res Function(GraphqlLoadingState) _then; - - @override - $Res call({ - Object result = freezed, - }) { - return _then(_value.copyWith( - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -abstract class _$GraphqlLoadingStateCopyWith - implements $GraphqlLoadingStateCopyWith { - factory _$GraphqlLoadingStateCopyWith(_GraphqlLoadingState value, - $Res Function(_GraphqlLoadingState) then) = - __$GraphqlLoadingStateCopyWithImpl; - @override - $Res call({QueryResult result}); -} - -class __$GraphqlLoadingStateCopyWithImpl - extends _$GraphqlLoadingStateCopyWithImpl - implements _$GraphqlLoadingStateCopyWith { - __$GraphqlLoadingStateCopyWithImpl(_GraphqlLoadingState _value, - $Res Function(_GraphqlLoadingState) _then) - : super(_value, (v) => _then(v as _GraphqlLoadingState)); - - @override - _GraphqlLoadingState get _value => super._value as _GraphqlLoadingState; - - @override - $Res call({ - Object result = freezed, - }) { - return _then(_GraphqlLoadingState( - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -class _$_GraphqlLoadingState extends _GraphqlLoadingState { - _$_GraphqlLoadingState({@required this.result}) - : assert(result != null), - super._(); - - @override - final QueryResult result; - - @override - String toString() { - return 'GraphqlLoadingState<$T>(result: $result)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other is _GraphqlLoadingState && - (identical(other.result, result) || - const DeepCollectionEquality().equals(other.result, result))); - } - - @override - int get hashCode => - runtimeType.hashCode ^ const DeepCollectionEquality().hash(result); - - @override - _$GraphqlLoadingStateCopyWith> get copyWith => - __$GraphqlLoadingStateCopyWithImpl>( - this, _$identity); -} - -abstract class _GraphqlLoadingState extends GraphqlLoadingState { - _GraphqlLoadingState._() : super._(); - factory _GraphqlLoadingState({@required QueryResult result}) = - _$_GraphqlLoadingState; - - @override - QueryResult get result; - @override - _$GraphqlLoadingStateCopyWith> get copyWith; -} - -class _$GraphqlErrorStateTearOff { - const _$GraphqlErrorStateTearOff(); - -// ignore: unused_element - _GraphqlErrorState call( - {@required OperationException error, @required QueryResult result}) { - return _GraphqlErrorState( - error: error, - result: result, - ); - } -} - -// ignore: unused_element -const $GraphqlErrorState = _$GraphqlErrorStateTearOff(); - -mixin _$GraphqlErrorState { - OperationException get error; - QueryResult get result; - - $GraphqlErrorStateCopyWith> get copyWith; -} - -abstract class $GraphqlErrorStateCopyWith { - factory $GraphqlErrorStateCopyWith(GraphqlErrorState value, - $Res Function(GraphqlErrorState) then) = - _$GraphqlErrorStateCopyWithImpl; - $Res call({OperationException error, QueryResult result}); -} - -class _$GraphqlErrorStateCopyWithImpl - implements $GraphqlErrorStateCopyWith { - _$GraphqlErrorStateCopyWithImpl(this._value, this._then); - - final GraphqlErrorState _value; - // ignore: unused_field - final $Res Function(GraphqlErrorState) _then; - - @override - $Res call({ - Object error = freezed, - Object result = freezed, - }) { - return _then(_value.copyWith( - error: error == freezed ? _value.error : error as OperationException, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -abstract class _$GraphqlErrorStateCopyWith - implements $GraphqlErrorStateCopyWith { - factory _$GraphqlErrorStateCopyWith(_GraphqlErrorState value, - $Res Function(_GraphqlErrorState) then) = - __$GraphqlErrorStateCopyWithImpl; - @override - $Res call({OperationException error, QueryResult result}); -} - -class __$GraphqlErrorStateCopyWithImpl - extends _$GraphqlErrorStateCopyWithImpl - implements _$GraphqlErrorStateCopyWith { - __$GraphqlErrorStateCopyWithImpl( - _GraphqlErrorState _value, $Res Function(_GraphqlErrorState) _then) - : super(_value, (v) => _then(v as _GraphqlErrorState)); - - @override - _GraphqlErrorState get _value => super._value as _GraphqlErrorState; - - @override - $Res call({ - Object error = freezed, - Object result = freezed, - }) { - return _then(_GraphqlErrorState( - error: error == freezed ? _value.error : error as OperationException, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -class _$_GraphqlErrorState extends _GraphqlErrorState { - _$_GraphqlErrorState({@required this.error, @required this.result}) - : assert(error != null), - assert(result != null), - super._(); - - @override - final OperationException error; - @override - final QueryResult result; - - @override - String toString() { - return 'GraphqlErrorState<$T>(error: $error, result: $result)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other is _GraphqlErrorState && - (identical(other.error, error) || - const DeepCollectionEquality().equals(other.error, error)) && - (identical(other.result, result) || - const DeepCollectionEquality().equals(other.result, result))); - } - - @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(error) ^ - const DeepCollectionEquality().hash(result); - - @override - _$GraphqlErrorStateCopyWith> get copyWith => - __$GraphqlErrorStateCopyWithImpl>( - this, _$identity); -} - -abstract class _GraphqlErrorState extends GraphqlErrorState { - _GraphqlErrorState._() : super._(); - factory _GraphqlErrorState( - {@required OperationException error, - @required QueryResult result}) = _$_GraphqlErrorState; - - @override - OperationException get error; - @override - QueryResult get result; - @override - _$GraphqlErrorStateCopyWith> get copyWith; -} - -class _$GraphqlLoadedStateTearOff { - const _$GraphqlLoadedStateTearOff(); - -// ignore: unused_element - _GraphqlLoadedState call( - {@required T data, @required QueryResult result}) { - return _GraphqlLoadedState( - data: data, - result: result, - ); - } -} - -// ignore: unused_element -const $GraphqlLoadedState = _$GraphqlLoadedStateTearOff(); - -mixin _$GraphqlLoadedState { - T get data; - QueryResult get result; - - $GraphqlLoadedStateCopyWith> get copyWith; -} - -abstract class $GraphqlLoadedStateCopyWith { - factory $GraphqlLoadedStateCopyWith(GraphqlLoadedState value, - $Res Function(GraphqlLoadedState) then) = - _$GraphqlLoadedStateCopyWithImpl; - $Res call({T data, QueryResult result}); -} - -class _$GraphqlLoadedStateCopyWithImpl - implements $GraphqlLoadedStateCopyWith { - _$GraphqlLoadedStateCopyWithImpl(this._value, this._then); - - final GraphqlLoadedState _value; - // ignore: unused_field - final $Res Function(GraphqlLoadedState) _then; - - @override - $Res call({ - Object data = freezed, - Object result = freezed, - }) { - return _then(_value.copyWith( - data: data == freezed ? _value.data : data as T, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -abstract class _$GraphqlLoadedStateCopyWith - implements $GraphqlLoadedStateCopyWith { - factory _$GraphqlLoadedStateCopyWith(_GraphqlLoadedState value, - $Res Function(_GraphqlLoadedState) then) = - __$GraphqlLoadedStateCopyWithImpl; - @override - $Res call({T data, QueryResult result}); -} - -class __$GraphqlLoadedStateCopyWithImpl - extends _$GraphqlLoadedStateCopyWithImpl - implements _$GraphqlLoadedStateCopyWith { - __$GraphqlLoadedStateCopyWithImpl(_GraphqlLoadedState _value, - $Res Function(_GraphqlLoadedState) _then) - : super(_value, (v) => _then(v as _GraphqlLoadedState)); - - @override - _GraphqlLoadedState get _value => super._value as _GraphqlLoadedState; - - @override - $Res call({ - Object data = freezed, - Object result = freezed, - }) { - return _then(_GraphqlLoadedState( - data: data == freezed ? _value.data : data as T, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -class _$_GraphqlLoadedState extends _GraphqlLoadedState { - _$_GraphqlLoadedState({@required this.data, @required this.result}) - : assert(data != null), - assert(result != null), - super._(); - - @override - final T data; - @override - final QueryResult result; - - @override - String toString() { - return 'GraphqlLoadedState<$T>(data: $data, result: $result)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other is _GraphqlLoadedState && - (identical(other.data, data) || - const DeepCollectionEquality().equals(other.data, data)) && - (identical(other.result, result) || - const DeepCollectionEquality().equals(other.result, result))); - } - - @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(data) ^ - const DeepCollectionEquality().hash(result); - - @override - _$GraphqlLoadedStateCopyWith> get copyWith => - __$GraphqlLoadedStateCopyWithImpl>( - this, _$identity); -} - -abstract class _GraphqlLoadedState extends GraphqlLoadedState { - _GraphqlLoadedState._() : super._(); - factory _GraphqlLoadedState( - {@required T data, - @required QueryResult result}) = _$_GraphqlLoadedState; - - @override - T get data; - @override - QueryResult get result; - @override - _$GraphqlLoadedStateCopyWith> get copyWith; -} - -class _$GraphqlRefetchStateTearOff { - const _$GraphqlRefetchStateTearOff(); - -// ignore: unused_element - _GraphqlRefetchState call({@required T data, QueryResult result}) { - return _GraphqlRefetchState( - data: data, - result: result, - ); - } -} - -// ignore: unused_element -const $GraphqlRefetchState = _$GraphqlRefetchStateTearOff(); - -mixin _$GraphqlRefetchState { - T get data; - QueryResult get result; - - $GraphqlRefetchStateCopyWith> get copyWith; -} - -abstract class $GraphqlRefetchStateCopyWith { - factory $GraphqlRefetchStateCopyWith(GraphqlRefetchState value, - $Res Function(GraphqlRefetchState) then) = - _$GraphqlRefetchStateCopyWithImpl; - $Res call({T data, QueryResult result}); -} - -class _$GraphqlRefetchStateCopyWithImpl - implements $GraphqlRefetchStateCopyWith { - _$GraphqlRefetchStateCopyWithImpl(this._value, this._then); - - final GraphqlRefetchState _value; - // ignore: unused_field - final $Res Function(GraphqlRefetchState) _then; - - @override - $Res call({ - Object data = freezed, - Object result = freezed, - }) { - return _then(_value.copyWith( - data: data == freezed ? _value.data : data as T, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -abstract class _$GraphqlRefetchStateCopyWith - implements $GraphqlRefetchStateCopyWith { - factory _$GraphqlRefetchStateCopyWith(_GraphqlRefetchState value, - $Res Function(_GraphqlRefetchState) then) = - __$GraphqlRefetchStateCopyWithImpl; - @override - $Res call({T data, QueryResult result}); -} - -class __$GraphqlRefetchStateCopyWithImpl - extends _$GraphqlRefetchStateCopyWithImpl - implements _$GraphqlRefetchStateCopyWith { - __$GraphqlRefetchStateCopyWithImpl(_GraphqlRefetchState _value, - $Res Function(_GraphqlRefetchState) _then) - : super(_value, (v) => _then(v as _GraphqlRefetchState)); - - @override - _GraphqlRefetchState get _value => super._value as _GraphqlRefetchState; - - @override - $Res call({ - Object data = freezed, - Object result = freezed, - }) { - return _then(_GraphqlRefetchState( - data: data == freezed ? _value.data : data as T, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -class _$_GraphqlRefetchState extends _GraphqlRefetchState { - _$_GraphqlRefetchState({@required this.data, this.result}) - : assert(data != null), - super._(); - - @override - final T data; - @override - final QueryResult result; - - @override - String toString() { - return 'GraphqlRefetchState<$T>(data: $data, result: $result)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other is _GraphqlRefetchState && - (identical(other.data, data) || - const DeepCollectionEquality().equals(other.data, data)) && - (identical(other.result, result) || - const DeepCollectionEquality().equals(other.result, result))); - } - - @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(data) ^ - const DeepCollectionEquality().hash(result); - - @override - _$GraphqlRefetchStateCopyWith> get copyWith => - __$GraphqlRefetchStateCopyWithImpl>( - this, _$identity); -} - -abstract class _GraphqlRefetchState extends GraphqlRefetchState { - _GraphqlRefetchState._() : super._(); - factory _GraphqlRefetchState({@required T data, QueryResult result}) = - _$_GraphqlRefetchState; - - @override - T get data; - @override - QueryResult get result; - @override - _$GraphqlRefetchStateCopyWith> get copyWith; -} - -class _$GraphqlFetchMoreStateTearOff { - const _$GraphqlFetchMoreStateTearOff(); - -// ignore: unused_element - _GraphqlFetchMoreState call({@required T data, QueryResult result}) { - return _GraphqlFetchMoreState( - data: data, - result: result, - ); - } -} - -// ignore: unused_element -const $GraphqlFetchMoreState = _$GraphqlFetchMoreStateTearOff(); - -mixin _$GraphqlFetchMoreState { - T get data; - QueryResult get result; - - $GraphqlFetchMoreStateCopyWith> get copyWith; -} - -abstract class $GraphqlFetchMoreStateCopyWith { - factory $GraphqlFetchMoreStateCopyWith(GraphqlFetchMoreState value, - $Res Function(GraphqlFetchMoreState) then) = - _$GraphqlFetchMoreStateCopyWithImpl; - $Res call({T data, QueryResult result}); -} - -class _$GraphqlFetchMoreStateCopyWithImpl - implements $GraphqlFetchMoreStateCopyWith { - _$GraphqlFetchMoreStateCopyWithImpl(this._value, this._then); - - final GraphqlFetchMoreState _value; - // ignore: unused_field - final $Res Function(GraphqlFetchMoreState) _then; - - @override - $Res call({ - Object data = freezed, - Object result = freezed, - }) { - return _then(_value.copyWith( - data: data == freezed ? _value.data : data as T, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -abstract class _$GraphqlFetchMoreStateCopyWith - implements $GraphqlFetchMoreStateCopyWith { - factory _$GraphqlFetchMoreStateCopyWith(_GraphqlFetchMoreState value, - $Res Function(_GraphqlFetchMoreState) then) = - __$GraphqlFetchMoreStateCopyWithImpl; - @override - $Res call({T data, QueryResult result}); -} - -class __$GraphqlFetchMoreStateCopyWithImpl - extends _$GraphqlFetchMoreStateCopyWithImpl - implements _$GraphqlFetchMoreStateCopyWith { - __$GraphqlFetchMoreStateCopyWithImpl(_GraphqlFetchMoreState _value, - $Res Function(_GraphqlFetchMoreState) _then) - : super(_value, (v) => _then(v as _GraphqlFetchMoreState)); - - @override - _GraphqlFetchMoreState get _value => - super._value as _GraphqlFetchMoreState; - - @override - $Res call({ - Object data = freezed, - Object result = freezed, - }) { - return _then(_GraphqlFetchMoreState( - data: data == freezed ? _value.data : data as T, - result: result == freezed ? _value.result : result as QueryResult, - )); - } -} - -class _$_GraphqlFetchMoreState extends _GraphqlFetchMoreState { - _$_GraphqlFetchMoreState({@required this.data, this.result}) - : assert(data != null), - super._(); - - @override - final T data; - @override - final QueryResult result; - - @override - String toString() { - return 'GraphqlFetchMoreState<$T>(data: $data, result: $result)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other is _GraphqlFetchMoreState && - (identical(other.data, data) || - const DeepCollectionEquality().equals(other.data, data)) && - (identical(other.result, result) || - const DeepCollectionEquality().equals(other.result, result))); - } - - @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(data) ^ - const DeepCollectionEquality().hash(result); - - @override - _$GraphqlFetchMoreStateCopyWith> get copyWith => - __$GraphqlFetchMoreStateCopyWithImpl>( - this, _$identity); -} - -abstract class _GraphqlFetchMoreState extends GraphqlFetchMoreState { - _GraphqlFetchMoreState._() : super._(); - factory _GraphqlFetchMoreState({@required T data, QueryResult result}) = - _$_GraphqlFetchMoreState; - - @override - T get data; - @override - QueryResult get result; - @override - _$GraphqlFetchMoreStateCopyWith> get copyWith; -} diff --git a/examples/flutter_bloc/lib/extended_bloc/repositories_bloc.dart b/examples/flutter_bloc/lib/extended_bloc/repositories_bloc.dart index ebb913014..e9030cbee 100644 --- a/examples/flutter_bloc/lib/extended_bloc/repositories_bloc.dart +++ b/examples/flutter_bloc/lib/extended_bloc/repositories_bloc.dart @@ -1,11 +1,8 @@ import 'package:gql/language.dart'; import 'package:graphql/client.dart'; +import 'package:graphql_flutter_bloc/graphql_flutter_bloc.dart'; -import 'package:graphql_flutter_bloc_example/extended_bloc/graphql/bloc.dart'; -import 'package:graphql_flutter_bloc_example/extended_bloc/graphql/event.dart'; -import 'package:graphql_flutter_bloc_example/extended_bloc/graphql/state.dart'; - -class RepositoriesBloc extends GraphqlBloc> { +class RepositoriesBloc extends QueryBloc> { static int defaultLimit = 5; RepositoriesBloc({GraphQLClient client, WatchQueryOptions options}) @@ -57,15 +54,18 @@ class RepositoriesBloc extends GraphqlBloc> { @override bool shouldFetchMore(int i, int threshold) { - return state is GraphqlLoadedState && - state.data['viewer']['repositories']['nodes'].length % - RepositoriesBloc.defaultLimit == - 0 && - i == state.data['viewer']['repositories']['nodes'].length - threshold; + return state.maybeWhen( + loaded: (data, result) { + return data['viewer']['repositories']['nodes'].length % + RepositoriesBloc.defaultLimit == + 0 && + i == data['viewer']['repositories']['nodes'].length - threshold; + }, + orElse: () => false); } void fetchMore({String after}) { - add(GraphqlFetchMoreEvent( + add(QueryEvent.fetchMore( options: FetchMoreOptions( variables: {'nRepositories': 5, 'after': after}, updateQuery: (dynamic previousResultData, dynamic fetchMoreResultData) { diff --git a/examples/flutter_bloc/lib/hive_init.dart b/examples/flutter_bloc/lib/hive_init.dart new file mode 100644 index 000000000..07d6334be --- /dev/null +++ b/examples/flutter_bloc/lib/hive_init.dart @@ -0,0 +1,34 @@ +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart' show WidgetsFlutterBinding; + +import 'package:hive/hive.dart' show Hive; +import 'package:path_provider/path_provider.dart' + show getApplicationDocumentsDirectory; +import 'package:path/path.dart' show join; + +import 'package:graphql/client.dart' show HiveStore; + +/// Initializes Hive with the path from [getApplicationDocumentsDirectory]. +/// +/// You can provide a [subDir] where the boxes should be stored. +/// +/// Extracted from [`hive_flutter` source][github] +/// +/// [github]: https://github.com/hivedb/hive/blob/5bf355496650017409fef4e9905e8826c5dc5bf3/hive_flutter/lib/src/hive_extensions.dart +Future initHiveForFlutter({ + String subDir, + Iterable boxes = const [HiveStore.defaultBoxName], +}) async { + if (!kIsWeb) { + var appDir = await getApplicationDocumentsDirectory(); + var path = appDir.path; + if (subDir != null) { + path = join(path, subDir); + } + Hive.init(path); + } + + for (var box in boxes) { + await Hive.openBox(box); + } +} diff --git a/examples/flutter_bloc/lib/main.dart b/examples/flutter_bloc/lib/main.dart index 654e9b015..0375167d8 100644 --- a/examples/flutter_bloc/lib/main.dart +++ b/examples/flutter_bloc/lib/main.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:graphql/client.dart'; -import 'package:graphql_flutter/graphql_flutter.dart' show initHiveForFlutter; import 'package:graphql_flutter_bloc_example/bloc.dart'; +import 'package:graphql_flutter_bloc_example/hive_init.dart'; import 'package:graphql_flutter_bloc_example/repository.dart'; import 'package:graphql_flutter_bloc_example/blocs/repos/my_repos_bloc.dart'; import 'package:graphql_flutter_bloc_example/extended_bloc/repositories_bloc.dart'; @@ -14,8 +14,10 @@ import 'package:graphql_flutter_bloc_example/extended_bloc.dart'; // ''; import 'local.dart'; -void main() async { +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); await initHiveForFlutter(); + runApp(MyApp()); } diff --git a/examples/flutter_bloc/pubspec.yaml b/examples/flutter_bloc/pubspec.yaml index 22add3fb4..dcd24b4c1 100644 --- a/examples/flutter_bloc/pubspec.yaml +++ b/examples/flutter_bloc/pubspec.yaml @@ -9,14 +9,14 @@ environment: dependencies: flutter: sdk: flutter - - graphql: ^4.0.0-alpha - graphql_flutter: ^4.0.0-alpha - + graphql: + path: ../../packages/graphql + graphql_flutter_bloc: ^0.4.4-beta.1 cupertino_icons: ^0.1.2 - flutter_bloc: ^5.0.1 - freezed_annotation: 0.11.0 - + flutter_bloc: ^6.0.5 + freezed_annotation: 0.12.0 + path_provider: ^1.6.18 + hive: ^1.4.4 equatable: ^0.2.0 dev_dependencies: @@ -24,13 +24,10 @@ dev_dependencies: sdk: flutter test: ^1.3.0 mockito: ^3.0.0 - freezed: ^0.11.4 flutter: uses-material-design: true dependency_overrides: - graphql_flutter: - path: ../../packages/graphql_flutter graphql: path: ../../packages/graphql diff --git a/examples/flutter_bloc/test/bloc_test.dart b/examples/flutter_bloc/test/bloc_test.dart index 0d4329bec..307fc6d31 100644 --- a/examples/flutter_bloc/test/bloc_test.dart +++ b/examples/flutter_bloc/test/bloc_test.dart @@ -68,6 +68,10 @@ void main() { ); }); + tearDown(() { + repoBloc.close(); + }); + test('initial state is loading', () { expect(repoBloc.state, ReposLoading()); }); From d0224c9779b88451f98aeec828509be3a806c67f Mon Sep 17 00:00:00 2001 From: micimize Date: Fri, 2 Oct 2020 15:06:43 -0500 Subject: [PATCH 118/118] update doc refs --- packages/graphql/README.md | 14 +++++++------- packages/graphql_flutter/README.md | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/graphql/README.md b/packages/graphql/README.md index 4e39c851f..c3c90865c 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -41,8 +41,8 @@ As of `v4`, it is built on foundational libraries from the [gql-dart project], i **Useful API Docs:** -- [`GraphQLCache`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLCache-class.html) -- [`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) (direct cache access) +- [`GraphQLCache`](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLCache-class.html) +- [`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLDataProxy-class.html) (direct cache access) ## Installation @@ -301,7 +301,7 @@ subscription.listen(reactToAddedReview) ### `client.watchQuery` and `ObservableQuery` -[`client.watchQuery`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLClient/watchQuery.html) +[`client.watchQuery`](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLClient/watchQuery.html) can be used to execute both queries and mutations, then reactively listen to changes to the underlying data in the cache. It is used in the `Query` and `Mutation` widgets of `graphql_flutter`: ```dart @@ -341,21 +341,21 @@ observableQuery.stream.listen((QueryResult result) { observableQuery.close(); ``` -`ObservableQuery` is a bit of a kitchen sink for reactive operation logic – consider looking at the [API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/ObservableQuery-class.html) if you'd like to develop a deeper understanding. +`ObservableQuery` is a bit of a kitchen sink for reactive operation logic – consider looking at the [API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/ObservableQuery-class.html) if you'd like to develop a deeper understanding. > **NB**: `watchQuery` and `ObservableQuery` currently don't have a nice APIs for `update` `onCompleted` and `onError` callbacks, > but you can have a look at how `graphql_flutter` registers them through -> [`onData`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/ObservableQuery/onData.html) in +> [`onData`](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/ObservableQuery/onData.html) in > [`Mutation.runMutation`](https://pub.dev/documentation/graphql_flutter/4.0.0-alpha.7/graphql_flutter/MutationState/runMutation.html). ## Direct Cache Access API -The [`GraphQLCache`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLCache-class.html) +The [`GraphQLCache`](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLCache-class.html) leverages [`normalize`] to give us a fairly apollo-ish [direct cache access] API, which is also available on `GraphQLClient`. This means we can do [local state management] in a similar fashion as well. A complete and well-commented rundown of can be found in the -[`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) +[`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLDataProxy-class.html) > **NB** You likely want to call the cache access API from your `client` for automatic broadcasting support. diff --git a/packages/graphql_flutter/README.md b/packages/graphql_flutter/README.md index d32459f83..09412eefc 100644 --- a/packages/graphql_flutter/README.md +++ b/packages/graphql_flutter/README.md @@ -39,8 +39,8 @@ This guide is mostly focused on setup, widgets, and flutter-specific considerati **Useful API Docs:** -- [`GraphQLCache`](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLCache-class.html) -- [`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) (direct cache access) +- [`GraphQLCache`](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLCache-class.html) +- [`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLDataProxy-class.html) (direct cache access) ## Installation @@ -289,7 +289,7 @@ Mutation( `GraphQLCache` allows for optimistic mutations by passing an `optimisticResult` to `RunMutation`. It will then call `update(GraphQLDataProxy cache, QueryResult result)` twice (once eagerly with `optimisticResult`), and rebroadcast all queries with the optimistic cache state. A complete and well-commented rundown of how exactly one interfaces with the `proxy` provided to `update` can be fount in the -[`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) +[`GraphQLDataProxy` API docs](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLDataProxy-class.html) ```dart ... @@ -433,7 +433,7 @@ class _MyHomePageState extends State { ### GraphQL Consumer If you want to use the `client` directly, say for some its -[direct cache update](https://pub.dev/documentation/graphql/4.0.0-alpha.7/graphql/GraphQLDataProxy-class.html) methods, +[direct cache update](https://pub.dev/documentation/graphql/4.0.0-alpha.11/graphql/GraphQLDataProxy-class.html) methods, You can use `GraphQLConsumer` to grab it from any `context` descended from a `GraphQLProvider`: ```dart