message: fix(flutter): compatible with `path_provider` usage

diff:
@@ -10,7 +10,14 @@ import 'package:graphql/src/cache/cache.dart';
import 'package:graphql/src/utilities/helpers.dart' show deeplyMergeLeft;
class InMemoryCache implements Cache {
- String masterKey = 'graphql_in_memory';
+ InMemoryCache({
+ this.storagePrefix = '',
+ }) {
+ masterKey = storagePrefix ?? '' + '_graphql_cache';
+ }
+
+ final String storagePrefix;
+ String masterKey;
@protected
HashMap<String, dynamic> data = HashMap<String, dynamic>();

diff:
@@ -10,10 +10,17 @@ import 'package:meta/meta.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<String> storagePrefix;
+
bool _writingToStorage = false;
@protected
@@ -65,7 +72,7 @@ class InMemoryCache implements Cache {
}
FutureOr<File> get _localStorageFile async {
- return File('cache.txt');
+ return File(join(await storagePrefix, 'cache.txt'));
}
Future<dynamic> _writeToStorage() async {

diff:
// @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<String> storagePrefix;
+
@protected
HashMap<String, dynamic> data = HashMap<String, dynamic>();
}

diff:
+import 'dart:async';
+
import 'package:meta/meta.dart';
import 'package:graphql/src/utilities/traverse.dart';
@@ -23,7 +25,8 @@ class NormalizedInMemoryCache extends InMemoryCache {
NormalizedInMemoryCache({
@required this.dataIdFromObject,
this.prefix = '@cache/reference',
- });
+ FutureOr<String> storagePrefix,
+ }) : super(storagePrefix: storagePrefix);
DataIdFromObject dataIdFromObject;

diff:
+import 'dart:async';
import 'dart:collection';
import 'package:meta/meta.dart';
@@ -61,9 +62,11 @@ class OptimisticCache extends NormalizedInMemoryCache {
OptimisticCache({
@required DataIdFromObject dataIdFromObject,
String prefix = '@cache/reference',
+ FutureOr<String> storagePrefix,
}) : super(
dataIdFromObject: dataIdFromObject,
prefix: prefix,
+ storagePrefix: storagePrefix,
);
@protected

diff:
+@TestOn("vm")
+
+import 'dart:async';
+import 'dart:io';
+import 'package:test/test.dart';
+import 'package:graphql/src/cache/in_memory_io.dart';
+
+void main() {
+ group('In memory exception handling', () {
+ test('FileSystemException', overridePrint((List<String> log) async {
+ final InMemoryCache cache = InMemoryCache(
+ storagePrefix: Future.error(FileSystemException()),
+ );
+ await cache.restore();
+ expect(
+ log,
+ ['Can\'t read file from storage, returning an empty HashMap.'],
+ );
+ }));
+ });
+}
+
+overridePrint(testFn(List<String> log)) => () {
+ final log = <String>[];
+ final spec = new ZoneSpecification(print: (_, __, ___, String msg) {
+ log.add(msg);
+ });
+ return Zone.current.fork(specification: spec).run(() => testFn(log));
+ };

diff:
import 'dart:async' show FutureOr;
-import 'dart:io' show Directory;
import 'package:meta/meta.dart';
import 'package:path_provider/path_provider.dart'
@@ -7,11 +6,11 @@ import 'package:path_provider/path_provider.dart'
import 'package:graphql/client.dart' as client;
-FutureOr<Directory> flutterStorageProvider() =>
- getApplicationDocumentsDirectory();
+final FutureOr<String> flutterStoragePrefix = (() async =>
+ (await getApplicationDocumentsDirectory()).path)();
class InMemoryCache extends client.InMemoryCache {
- InMemoryCache() : super(storageProvider: flutterStorageProvider);
+ InMemoryCache() : super(storagePrefix: flutterStoragePrefix);
}
class NormalizedInMemoryCache extends client.NormalizedInMemoryCache {
@@ -21,7 +20,7 @@ class NormalizedInMemoryCache extends client.NormalizedInMemoryCache {
}) : super(
dataIdFromObject: dataIdFromObject,
prefix: prefix,
- storageProvider: flutterStorageProvider,
+ storagePrefix: flutterStoragePrefix,
);
}
@@ -32,6 +31,6 @@ class OptimisticCache extends client.OptimisticCache {
}) : super(
dataIdFromObject: dataIdFromObject,
prefix: prefix,
- storageProvider: flutterStorageProvider,
+ storagePrefix: flutterStoragePrefix,
);
}


message: test(client): first integration test websocket

change_type: MODIFY

diff:
import 'dart:async';
import 'dart:convert';
-import 'dart:io';
+import 'package:websocket/websocket.dart' show WebSocket, WebSocketStatus;
import 'package:rxdart/rxdart.dart';
import 'package:uuid/uuid.dart';
@@ -13,7 +13,6 @@ class SocketClientConfig {
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
@@ -40,8 +39,6 @@ class SocketClientConfig {
// 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;
@@ -118,12 +115,13 @@ class SocketClient {
_socket = await WebSocket.connect(url,
protocols: protocols,
headers: headers,
- compression: config.compression);
+ );
_connectionStateController.value = SocketConnectionState.CONNECTED;
print('Connected to websocket.');
_write(config.initOperation);
_messageStream = _socket
+ .stream
.asBroadcastStream()
.map<GraphQLSocketMessage>(_parseSocketMessage);
@@ -154,11 +152,14 @@ class SocketClient {
print('error: $e');
});
} catch (e) {
- onConnectionLost();
+ onConnectionLost(e);
}
}
- void onConnectionLost() {
+ void onConnectionLost([e]) {
+ if(e != null) {
+ print('There was an error causing connection lost: $e');
+ }
print('Disconnected from websocket.');
_reconnectTimer?.cancel();
_keepAliveSubscription?.cancel();
@@ -199,10 +200,12 @@ class SocketClient {
Future<void> dispose() async {
print('Disposing socket client..');
_reconnectTimer?.cancel();
- await _socket?.close();
- await _keepAliveSubscription?.cancel();
- await _messageSubscription?.cancel();
- await _connectionStateController?.close();
+ await Future.wait([
+ _socket?.close(),
+ _keepAliveSubscription?.cancel(),
+ _messageSubscription?.cancel(),
+ _connectionStateController?.close(),
+ ]);
}
static GraphQLSocketMessage _parseSocketMessage(dynamic message) {

diff:
@@ -17,6 +17,7 @@ dependencies:
uuid: ^2.0.1
graphql_parser: ^1.1.3
rxdart: ^0.22.0
+ websocket: ^0.0.2
dev_dependencies:
pedantic: <=1.7.99
mockito: ^4.0.0

change_type: ADD

diff:
+import 'package:test/test.dart';
+// import 'dart:html' show WebSocket, querySelector;
+import 'package:graphql/src/socket_client.dart'
+ show SocketClient, SocketConnectionState;
+
+void main() {
+ group('SocketClient', () {
+ test('connection', () async {
+ final c = SocketClient('ws://echo.websocket.org');
+ await expectLater(
+ c.connectionState.asBroadcastStream(),
+ emitsInOrder(
+ [
+ SocketConnectionState.CONNECTING,
+ SocketConnectionState.CONNECTED,
+ ],
+ ),
+ );
+ await c.dispose();
+ }, tags: "integration");
+ });
+}

license: MIT License

repo: zino-hofmann/graphql-flutter


message:
test(client): integration test websocket
This test can be run both on `vm` and `chrome`, as now we are using a package to have universal API for websocket.

diff:
@@ -2,7 +2,6 @@ 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/utilities/helpers.dart';
import 'package:graphql/src/websocket/messages.dart';
import 'package:meta/meta.dart';
@@ -23,7 +22,6 @@ 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() {
@@ -31,7 +29,6 @@ class WebSocketLink extends Link {
}
final String url;
- final Map<String, dynamic> headers;
final bool reconnectOnHeaderChange;
final SocketClientConfig config;
@@ -44,17 +41,8 @@ class WebSocketLink extends Link {
if (context != null && context.containsKey('headers')) {
concatHeaders.addAll(context['headers'] as Map<String, dynamic>);
}
- if (headers != null) {
- concatHeaders.addAll(headers);
- }
- if (_socketClient == null ||
- (reconnectOnHeaderChange &&
- areDifferentVariables(_socketClient.headers, concatHeaders))) {
- if (_socketClient != null) {
- print(
- 'Creating a new socket client, because the headers have changed..');
- }
+ if (_socketClient == null) {
connectOrReconnect(headers: concatHeaders);
}
@@ -70,7 +58,7 @@ class WebSocketLink extends Link {
void connectOrReconnect({Map<String, dynamic> headers}) {
_socketClient?.dispose();
_socketClient =
- SocketClient(url, headers: headers ?? this.headers, config: config);
+ SocketClient(url, config: config);
}
/// Disposes the underlying socket client explicitly. Only use this, if you want to disconnect from

diff:
import 'dart:async';
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/uuid.dart';
+import 'package:uuid_enhanced/uuid.dart';
import 'package:graphql/src/websocket/messages.dart';
@@ -77,24 +79,24 @@ class SocketClient {
this.protocols = const <String>[
'graphql-ws',
],
- this.headers = const <String, String>{
- 'content-type': 'application/json',
- },
this.config = const SocketClientConfig(),
+ @visibleForTesting
+ this.randomBytesForUuid,
}) {
_connect();
}
- final Uuid _uuid = Uuid();
+ Uint8List randomBytesForUuid;
final String url;
final SocketClientConfig config;
final Iterable<String> protocols;
- final Map<String, dynamic> headers;
final BehaviorSubject<SocketConnectionState> _connectionStateController =
BehaviorSubject<SocketConnectionState>();
Timer _reconnectTimer;
WebSocket _socket;
+ @visibleForTesting
+ WebSocket get socket => _socket;
Stream<GraphQLSocketMessage> _messageStream;
StreamSubscription<ConnectionKeepAlive> _keepAliveSubscription;
@@ -114,7 +116,6 @@ class SocketClient {
try {
_socket = await WebSocket.connect(url,
protocols: protocols,
- headers: headers,
);
_connectionStateController.value = SocketConnectionState.CONNECTED;
print('Connected to websocket.');
@@ -122,7 +123,6 @@ class SocketClient {
_messageStream = _socket
.stream
- .asBroadcastStream()
.map<GraphQLSocketMessage>(_parseSocketMessage);
if (config.inactivityTimeout != null) {
@@ -256,7 +256,7 @@ class SocketClient {
/// In case of socket disconnection, the returned stream will be closed.
Stream<SubscriptionData> subscribe(
final SubscriptionRequest payload, final bool waitForConnection) {
- final String id = _uuid.v4();
+ final String id = Uuid.randomUuid(random: randomBytesForUuid).toString();
final StreamController<SubscriptionData> response =
StreamController<SubscriptionData>();
StreamSubscription<SocketConnectionState> sub;

diff:
@@ -189,6 +189,13 @@ class SubscriptionData extends GraphQLSocketMessage {
'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

diff:
@@ -14,10 +14,10 @@ dependencies:
mime: ^0.9.6+2
path: ^1.6.2
http_parser: ^3.1.3
- uuid: ^2.0.1
+ uuid_enhanced: ^3.0.2
graphql_parser: ^1.1.3
rxdart: ^0.22.0
- websocket: ^0.0.2
+ websocket: ^0.0.3
dev_dependencies:
pedantic: <=1.7.99
mockito: ^4.0.0

diff:
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:graphql/src/link/operation.dart';
+import 'package:graphql/src/websocket/messages.dart';
import 'package:test/test.dart';
-// import 'dart:html' show WebSocket, querySelector;
import 'package:graphql/src/socket_client.dart'
show SocketClient, SocketConnectionState;
void main() {
group('SocketClient', () {
+ SocketClient socketClient;
+ setUp(() {
+ 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(() async {
+ await socketClient.dispose();
+ });
test('connection', () async {
- final c = SocketClient('ws://echo.websocket.org');
await expectLater(
- c.connectionState.asBroadcastStream(),
+ socketClient.connectionState.asBroadcastStream(),
emitsInOrder(
[
SocketConnectionState.CONNECTING,
@@ -16,7 +31,43 @@ void main() {
],
),
);
- await c.dispose();
- }, tags: "integration");
});
+ test('subscription data', () async {
+ final payload = SubscriptionRequest(Operation(document: 'empty'));
+ 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 ==
+ '{"type":"start","id":"01020304-0506-4708-890a-0b0c0d0e0f10","payload":{"operationName":null,"query":"empty","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");
}

diff:
@@ -26,8 +26,7 @@ class GraphQLWidgetScreen extends StatelessWidget {
getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
);
- // TODO don't think we have to cast here, maybe covariant
- Link link = authLink.concat(httpLink as Link);
+ Link link = authLink.concat(httpLink);
if (ENABLE_WEBSOCKETS) {
final WebSocketLink websocketLink = WebSocketLink(
url: 'ws://localhost:8080/ws/graphql',


message: ci(client): browser test fail on the first compiled file

diff:
@@ -102,6 +102,8 @@ jobs:
cd packages/graphql
pub global activate coverage
pub get
+ # warm-up dart2js, this is a dirty workaround
+ dart2js test/websocket_test.dart
pub run test test -p chromium
pub run test_coverage
- run:


message: ci(client): fix lint and clean up work-around

diff:
@@ -102,7 +102,6 @@ jobs:
cd packages/graphql
pub global activate coverage
pub get
- dart2js test/websocket_test.dart
pub run test_coverage
# cannot `run pub run test test/.test_coverage.dart -p chromium`
# it randomly fails compiling some dart files to js

diff:
@@ -2,8 +2,6 @@ import 'dart:async';
import 'package:test/test.dart';
import 'package:graphql/src/cache/in_memory.dart';
-import 'helpers.dart';
-
const String aKey = 'aKey';
const String bKey = 'bKey';
const String cKey = 'cKey';

diff:
@@ -14,8 +14,10 @@ void main() {
group(
'SocketClient',
() {
+ // ignore: deprecated_member_use_from_same_package
SocketClient socketClient;
setUp(() {
+ // ignore: deprecated_member_use_from_same_package
socketClient = SocketClient(
'ws://echo.websocket.org',
protocols: null,

diff:
@@ -8,6 +8,7 @@ import 'helpers.dart';
void main() {
group('Link Websocket', () {
test('simple connection', overridePrint((List<String> log) {
+ // ignore: deprecated_member_use_from_same_package
WebSocketLink(
url: 'ws://echo.websocket.org',
// ignore: deprecated_member_use_from_same_package


message: ci: code coverage badge per package

diff:
@@ -117,7 +117,9 @@ jobs:
name: Upload coverage
command: |
bash <(curl -s https://codecov.io/bash) -F graphql_flutter
+ bash <(curl -s https://codecov.io/bash) -F graphql_flutter -B `git rev-parse --abbrev-ref HEAD`_pseudo_branch_package_graphql_flutter
bash <(curl -s https://codecov.io/bash) -F graphql_client
+ bash <(curl -s https://codecov.io/bash) -F graphql_client -B `git rev-parse --abbrev-ref HEAD`_pseudo_branch_package_graphql_client
release:
docker: *node_environment
steps:

original_message: ci: code coverage badge per package


message:
fix(client): crashed upload file
Beside fixing the crash (close this commit also have flashing deprecation warning,
and clean up logs during test.

diff:
+tags:
+ integration:
+
define_platforms:
chromium:
name: Chromium

diff:
@@ -174,7 +174,7 @@ Future<BaseRequest> _prepareRequest(
// @deprecated, backward compatible only
// in case the body is io.File
// in future release, io.File will no longer be supported
- if (isIoFile(body)) {
+ if (isIoFile(object)) {
return null;
}
return object.toJson();

diff:
@@ -5,10 +5,19 @@ 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<MultipartFile> multipartFileFrom(io.File f) async => MultipartFile(
'',
f.openRead(),
await f.length(),
- contentType: MediaType.parse(lookupMimeType(f.path)),
+ contentType: contentType(f),
filename: basename(f.path),
);

diff:
@@ -5,6 +5,8 @@ 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<String> log) async {
@@ -19,11 +21,3 @@ void main() {
}));
});
}
-
-overridePrint(testFn(List<String> log)) => () {
- final log = <String>[];
- final spec = new ZoneSpecification(print: (_, __, ___, String msg) {
- log.add(msg);
- });
- return Zone.current.fork(specification: spec).run(() => testFn(log));
- };

diff:
@@ -7,20 +7,22 @@ import 'package:test/test.dart';
import 'package:graphql/src/socket_client.dart'
show SocketClient, SocketConnectionState;
+import 'helpers.dart';
+
void main() {
group('SocketClient', () {
SocketClient socketClient;
- setUp(() {
+ 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(() async {
+ }));
+ tearDown(overridePrint((log) async {
await socketClient.dispose();
- });
+ }));
test('connection', () async {
await expectLater(
socketClient.connectionState.asBroadcastStream(),

diff:
@@ -10,13 +10,15 @@ 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(() {
+ setUp(overridePrint((log) {
// ignore: deprecated_member_use_from_same_package
socketClient = SocketClient(
'ws://echo.websocket.org',
@@ -24,10 +26,10 @@ void main() {
randomBytesForUuid: Uint8List.fromList(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
);
- });
- tearDown(() async {
+ }));
+ tearDown(overridePrint((log) async {
await socketClient.dispose();
- });
+ }));
test('connection', () async {
await expectLater(
socketClient.connectionState.asBroadcastStream(),

original_message:
fix(client): crashed upload file

Beside fixing the crash (close #336), this commit also have flashing deprecation warning,
and clean up logs during test.


message:
fix(client): error handling
Fixing that some errors/exceptions that is not related to GraphQL are still coerced and have no useful information.

diff:
@@ -233,6 +233,8 @@ class QueryManager {
// so try/catch to avoid "could not access message"
try {
errorMessage = error.message as String;
+ assert(errorMessage != null);
+ assert(errorMessage.isNotEmpty);
} catch (e) {
throw error;
}

diff:
@@ -128,7 +128,32 @@ void main() {
expect(nodes[2]['viewerHasStarred'], false);
return;
});
-// test('failed query because of network', {});
+
+ test('failed query because of an exception with null string', () async {
+ final e = Exception();
+ when(mockHttpClient.send(any)).thenAnswer((_) async {
+ throw e;
+ });
+
+ final Future<QueryResult> r = graphQLClientClient
+ .query(WatchQueryOptions(document: readRepositories));
+
+ await expectLater(r, throwsA(e));
+ return;
+ });
+
+ test('failed query because of an exception with empty string', () async {
+ final e = Exception('');
+ when(mockHttpClient.send(any)).thenAnswer((_) async {
+ throw e;
+ });
+
+ final Future<QueryResult> r = graphQLClientClient
+ .query(WatchQueryOptions(document: readRepositories));
+
+ await expectLater(r, throwsA(e));
+ return;
+ });
// test('failed query because of because of error response', {});
// test('failed query because of because of invalid response', () {
// String responseBody =

original_message:
fix(client): error handling

Fixing https://github.com/zino-app/graphql-flutter/issues/355 that some errors/exceptions that is not related to GraphQL are still coerced and have no useful information.


message: ci: add weekly releases

diff:
@@ -201,6 +201,31 @@ jobs:
cd packages/graphql_flutter
pub publish -f
workflows:
+ weekly:
+ triggers:
+ - schedule:
+ cron: "0 21 * * 1"
+ filters:
+ branches:
+ only:
+ - master
+ - beta
+ jobs:
+ - dependencies
+ - lint:
+ requires:
+ - dependencies
+ - coverage:
+ requires:
+ - dependencies
+ - release:
+ requires:
+ - dependencies
+ - lint
+ - coverage
+ - sync_with_beta:
+ requires:
+ - release
default:
jobs:
- dependencies:

original_message: ci: add weekly releases


message: ci: add nightly builds

diff:
@@ -201,6 +201,30 @@ jobs:
cd packages/graphql_flutter
pub publish -f
workflows:
+ nightly:
+ triggers:
+ - schedule:
+ cron: "0 0 * * *"
+ filters:
+ branches:
+ only:
+ - beta
+ jobs:
+ - dependencies
+ - lint:
+ requires:
+ - dependencies
+ - coverage:
+ requires:
+ - dependencies
+ - release:
+ requires:
+ - dependencies
+ - lint
+ - coverage
+ - sync_with_beta:
+ requires:
+ - release
weekly:
triggers:
- schedule:
@@ -209,7 +233,6 @@ workflows:
branches:
only:
- master
- - beta
jobs:
- dependencies
- lint:
@@ -244,23 +267,6 @@ workflows:
filters:
tags:
ignore: /^.*$/
- - release:
- requires:
- - dependencies
- - lint
- - coverage
- filters:
- branches:
- only:
- - master
- - beta
- - sync_with_beta:
- requires:
- - release
- filters:
- branches:
- only:
- - master
- publish:
requires:
- dependencies

original_message: ci: add nightly builds


message: build: pipeline maintainance

diff:
@@ -126,7 +126,7 @@ jobs:
- checkout
- run:
name: Generate release tags
- command: npx semantic-release@beta
+ command: npx semantic-release@beta@16.0.0-beta.22
sync_with_beta:
docker: *node_environment
steps:

diff:
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: 2.1.1-beta.2
authors:
@@ -18,11 +19,11 @@ dependencies:
graphql_parser: ^1.1.3
rxdart: ^0.22.0
websocket: ^0.0.5
- quiver: '>=2.0.0 <3.0.0'
+ quiver: ">=2.0.0 <3.0.0"
dev_dependencies:
- pedantic: <=1.7.99
+ pedantic: ^1.8.0+1
mockito: ^4.0.0
test: ^1.5.3
test_coverage: ^0.2.0
environment:
- sdk: '>=2.2.2 <3.0.0'
+ sdk: ">=2.2.2 <3.0.0"

diff:
@@ -8,7 +8,7 @@ import 'package:graphql/client.dart';
overridePrint(testFn(List<String> log)) => () {
final log = <String>[];
- final spec = new ZoneSpecification(print: (_, __, ___, String msg) {
+ final spec = ZoneSpecification(print: (_, __, ___, String msg) {
log.add(msg);
});
return Zone.current.fork(specification: spec).run(() => testFn(log));

diff:
@@ -10,7 +10,7 @@ dependencies:
path: ..
dev_dependencies:
- pedantic: "<=1.7.99"
+ pedantic: ^1.8.0+1
flutter_test:
sdk: flutter
test: ^1.5.1

diff:
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: 2.1.1-beta.2
authors:
@@ -16,10 +17,10 @@ dependencies:
path_provider: ^1.1.0
rxdart: ^0.22.0
dev_dependencies:
- pedantic: <=1.7.99
+ pedantic: ^1.8.0+1
mockito: ^4.0.0
flutter_test:
sdk: flutter
test: ^1.5.3
environment:
- sdk: '>=2.2.2 <3.0.0'
+ sdk: ">=2.2.2 <3.0.0"

original_message: build: pipeline maintainance


message: build: upgrade test_coverage

diff:
@@ -103,7 +103,7 @@ jobs:
pub global activate coverage
pub get
pub run test_coverage
- # cannot `run pub run test test/.test_coverage.dart -p chromium`
+ # cannot run `pub run test test/.test_coverage.dart -p chromium`
# it randomly fails compiling some dart files to js
# test/.test_coverage.dart is a by product of running
# `pub run test_coverage`

original_message: build: upgrade test_coverage


message: ci: use recomended way to run  test_coverage

diff:
@@ -103,11 +103,7 @@ jobs:
pub global activate coverage
pub get
pub run test_coverage
- # cannot run `pub run test test/.test_coverage.dart -p chromium`
- # it randomly fails compiling some dart files to js
- # test/.test_coverage.dart is a by product of running
- # `pub run test_coverage`
- pub run test test/.test_coverage.dart -p chromium
+ dart --enable_asserts --enable-vm-service test/.test_coverage.dart
- run:
name: Code coverage (graphql_flutter)
command: |


message: ci: allow coverage to drop by up to 1%

diff:
flags:
- graphql_flutter:
+ - graphql_flutter:
paths:
- packages/graphql_flutter/
- graphql_client:
+ - graphql_client:
paths:
- packages/graphql/
+comment:
+ require_changes: true # Only post a comment if coverage changes.
+coverage:
+ status:
+ project:
+ default:
+ threshold: 1% # Allow coverage to drop by up to 1% in a PR before marking it failed
+ patch: off

original_message: ci: allow coverage to drop by up to 1%


message: ci: bump the `semantic-release` version

diff:
@@ -122,7 +122,7 @@ jobs:
- checkout
- run:
name: Generate release tags
- command: npx semantic-release@beta@16.0.0-beta.22
+ command: npx semantic-release@beta@16.0.0-beta.24
sync_with_beta:
docker: *node_environment
steps:
@@ -218,9 +218,6 @@ workflows:
- dependencies
- lint
- coverage
- - sync_with_beta:
- requires:
- - release
weekly:
triggers:
- schedule:

original_message: ci: bump the `semantic-release` version


message: docs: include a changelog

diff:
@@ -122,7 +122,7 @@ jobs:
- checkout
- run:
name: Generate release tags
- command: npx semantic-release@16.0.0-beta.24
+ command: npx -p @semantic-release/changelog -p @semantic-release/git -p semantic-release@16.0.0-beta.31 semantic-release
sync_with_beta:
docker: *node_environment
steps:

diff:
@@ -2,8 +2,15 @@ branches:
- master
- name: beta
prerelease: true
- channel: beta
plugins:
- - '@semantic-release/commit-analyzer'
- - '@semantic-release/release-notes-generator'
- - '@semantic-release/github'
+ - "@semantic-release/commit-analyzer"
+ - "@semantic-release/release-notes-generator"
+ - - "@semantic-release/changelog"
+ - changelogFile: "packages/graphql/CHANGELOG.md"
+ - - "@semantic-release/changelog"
+ - changelogFile: "packages/graphql_flutter/CHANGELOG.md"
+ - - "@semantic-release/git"
+ - assets:
+ - "packages/graphql/CHANGELOG.md"
+ - "packages/graphql_flutter/CHANGELOG.md"
+ - "@semantic-release/github"

original_message: docs: include a changelog


message: ci: bump semantic-release to the latest beta

diff:
@@ -122,7 +122,7 @@ jobs:
- checkout
- run:
name: Generate release tags
- command: npx -p @semantic-release/changelog -p @semantic-release/git -p semantic-release@16.0.0-beta.31 semantic-release
+ command: npx -p @semantic-release/changelog -p @semantic-release/git -p semantic-release@16.0.0-beta.46 semantic-release
sync_with_beta:
docker: *node_environment
steps:


message: ci: upgrade to the latest version

diff:
@@ -122,7 +122,7 @@ jobs:
- checkout
- run:
name: Generate release tags
- command: npx -p @semantic-release/changelog -p @semantic-release/git -p semantic-release@16.0.0-beta.46 semantic-release
+ command: npx -p @semantic-release/changelog -p @semantic-release/git@7.1.0-beta.11 -p semantic-release@16.0.0-beta.46 semantic-release
sync_with_beta:
docker: *node_environment
steps:

original_message: ci: upgrade to the latest @semantic-release/git version


message: ci: fix breaking change in `semantic-release` when channels are not included in the version tag

diff:
@@ -151,7 +151,7 @@ jobs:
command: |
cd packages/graphql
- VERSION=${CIRCLE_TAG%"@beta"}
+ VERSION=${CIRCLE_TAG}
VERSION=${VERSION#"v"}
# patch version
@@ -163,7 +163,7 @@ jobs:
command: |
cd packages/graphql_flutter
- VERSION=${CIRCLE_TAG%"@beta"}
+ VERSION=${CIRCLE_TAG}
VERSION=${VERSION#"v"}
# patch version
@@ -178,7 +178,7 @@ jobs:
- run:
name: Bump version numbers
command: |
- if [[ $CIRCLE_TAG =~ ^.*@beta.*$ ]]; then BRANCH=beta; else BRANCH=master; fi
+ if [[ $CIRCLE_TAG =~ ^.*beta.*$ ]]; then BRANCH=beta; else BRANCH=master; fi
git checkout $BRANCH
git add packages/graphql/pubspec.yaml packages/graphql_flutter/pubspec.yaml


message: feat: Expose `possibleTypes` on cache

diff:
@@ -17,6 +17,9 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
/// `typePolicies` to pass down to `normalize`
Map<String, TypePolicy> get typePolicies;
+ /// `possibleTypes` to pass down to [normalize]
+ Map<String, Set<String>> get possibleTypes;
+
/// Optional `dataIdFromObject` function to pass through to [normalize]
DataIdResolver? get dataIdFromObject;
@@ -92,6 +95,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
document: request.operation.document,
operationName: request.operation.operationName,
variables: sanitizeVariables(request.variables)!,
+ possibleTypes: possibleTypes,
);
Map<String, dynamic>? readFragment(
@@ -112,6 +116,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
idFields: fragmentRequest.idFields,
fragmentName: fragmentRequest.fragment.fragmentName,
variables: sanitizeVariables(fragmentRequest.variables)!,
+ possibleTypes: possibleTypes,
);
void writeQuery(
@@ -134,6 +139,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
variables: sanitizeVariables(request.variables)!,
// data
data: data,
+ possibleTypes: possibleTypes,
);
if (broadcast ?? true) {
broadcastRequested = true;
@@ -171,6 +177,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
variables: sanitizeVariables(request.variables)!,
// data
data: data,
+ possibleTypes: possibleTypes,
);
if (broadcast ?? true) {
broadcastRequested = true;

diff:
@@ -44,6 +44,9 @@ class OptimisticProxy extends NormalizingDataProxy {
/// `typePolicies` to pass down to `normalize` (proxied from [cache])
get typePolicies => cache.typePolicies;
+ /// `possibleTypeOf` to pass down to `normalize` (proxied from [cache])
+ get possibleTypes => cache.possibleTypes;
+
/// Optional `dataIdFromObject` function to pass through to [normalize]
/// (proxied from [cache])
get dataIdFromObject => cache.dataIdFromObject;

diff:
@@ -32,6 +32,7 @@ class GraphQLCache extends NormalizingDataProxy {
Store? store,
this.dataIdFromObject,
this.typePolicies = const {},
+ this.possibleTypes = const {},
this.partialDataPolicy = PartialDataCachePolicy.acceptForOptimisticData,
Object? Function(Object?) sanitizeVariables = sanitizeFilesForCache,
}) : sanitizeVariables = variableSanitizer(sanitizeVariables),
@@ -60,6 +61,9 @@ class GraphQLCache extends NormalizingDataProxy {
/// `typePolicies` to pass down to [normalize]
final Map<String, TypePolicy> typePolicies;
+ /// `possibleTypes` to pass down to [normalize]
+ final Map<String, Set<String>> possibleTypes;
+
/// Optional `dataIdFromObject` function to pass through to [normalize]
final DataIdResolver? dataIdFromObject;

diff:
@@ -13,7 +13,7 @@ dependencies:
gql_error_link: ^0.2.0
gql_dedupe_link: ^2.0.0
hive: ^2.0.0
- normalize: ^0.5.1
+ normalize: ^0.5.3
http: ^0.13.0
collection: ^1.15.0
web_socket_channel: ^2.0.0

original_message: feat: Expose `possibleTypes` on cache (#906)


message:
feat: Update normalize
Update normalize to `0.6.0` and update documentation.

diff:
@@ -422,6 +422,79 @@ String customDataIdFromObject(Map<String, Object> data) {
}
```
+Normalize requires you to specify the possible types map for fragments to work correctly. This
+is a mapping from abstract union and interface types to their concrete object types. E.g. take the
+schema
+
+```graphql
+
+interface PersonI {
+ name: String
+ age: Int
+}
+
+type Employee implements PersonI {
+ name: String
+ age: Int
+ daysOfEmployement: Int
+}
+
+type InStoreCustomer implements PersonI {
+ name: String
+ age: Int
+ numberOfPurchases: Int
+}
+
+type OnlineCustomer implements PersonI {
+ name: String
+ age: Int
+ numberOfPurchases: Int
+}
+
+union CustomerU = OnlineCustomer | InStoreCustomer
+
+```
+
+the possible types map would be:
+
+```dart
+const POSSIBLE_TYPES = const {
+ 'CustomerU': {'InStoreCustomer', 'OnlineCustomer'},
+ 'PersonI': {'Employee', 'InStoreCustomer', 'OnlineCustomer'},
+}
+
+// Here's how it's parsed to the cache
+final client = GraphQLClient(
+ cache: GraphQLCache(
+ possibleTypes: POSSIBLE_TYPES,
+ ),
+)
+```
+
+You can generate the `POSSIBLE_TYPES` map, e.g., using [graphql_codegen](https://pub.dev/packages/graphql_codegen).
+
+Furthermore, for normalize to correctly resolve the type you should always make sure you're querying the `__typename`. Given the example above a query could look something like
+
+```graphql
+
+query {
+ people {
+ __typename # Needed to decide where which entry to update in the cache
+ ... on Employee {
+ name
+ age
+ }
+ ... on Customer {
+ name
+ age
+ }
+ }
+}
+
+```
+
+if you're not providing the possible type map and introspecting the typename, the cache can't be updated.
+
## Direct Cache Access API
The [`GraphQLCache`](https://pub.dev/documentation/graphql/latest/graphql/GraphQLCache-class.html)

diff:
@@ -13,7 +13,7 @@ dependencies:
gql_error_link: ^0.2.0
gql_dedupe_link: ^2.0.0
hive: ^2.0.0
- normalize: ^0.5.3
+ normalize: ^0.6.0
http: ^0.13.0
collection: ^1.15.0
web_socket_channel: ^2.0.0


message: feat: Add parser support

diff:
@@ -3,14 +3,16 @@ import 'package:graphql/src/core/_data_class.dart';
import 'package:gql/ast.dart';
import 'package:graphql/client.dart';
+import 'package:graphql/src/core/result_parser.dart';
/// TODO refactor into [Request] container
/// Base options.
-abstract class BaseOptions extends MutableDataClass {
+abstract class BaseOptions<TParsed> extends MutableDataClass {
BaseOptions({
required this.document,
this.variables = const {},
this.operationName,
+ ResultParserFn<TParsed>? parserFn,
Context? context,
FetchPolicy? fetchPolicy,
ErrorPolicy? errorPolicy,
@@ -21,7 +23,11 @@ abstract class BaseOptions extends MutableDataClass {
error: errorPolicy,
cacheReread: cacheRereadPolicy,
),
- context = context ?? Context();
+ context = context ?? Context(),
+ parserFn = parserFn ??
+ ((d) => throw UnimplementedError(
+ "Please provide a parser function to support result parsing.",
+ ));
/// Document containing at least one [OperationDefinitionNode]
DocumentNode document;
@@ -50,6 +56,8 @@ abstract class BaseOptions extends MutableDataClass {
/// Context to be passed to link execution chain.
Context context;
+ ResultParserFn<TParsed> parserFn;
+
// TODO consider inverting this relationship
/// Resolve these options into a request
Request get asRequest => Request(

diff:
@@ -12,11 +12,11 @@ import 'package:graphql/src/core/_query_write_handling.dart';
///
/// This is the **Internal Implementation**,
/// used by [ObservableQuery] and [GraphQLCLient.fetchMore]
-Future<QueryResult> fetchMoreImplementation(
+Future<QueryResult<TParsed>> fetchMoreImplementation<TParsed>(
FetchMoreOptions fetchMoreOptions, {
- required QueryOptions originalOptions,
+ required QueryOptions<TParsed> originalOptions,
required QueryManager queryManager,
- required QueryResult previousResult,
+ required QueryResult<TParsed> previousResult,
String? queryId,
}) async {
// fetch more and udpate
@@ -24,7 +24,7 @@ Future<QueryResult> fetchMoreImplementation(
final document = (fetchMoreOptions.document ?? originalOptions.document);
final request = originalOptions.asRequest;
- final combinedOptions = QueryOptions(
+ final combinedOptions = QueryOptions<TParsed>(
fetchPolicy: FetchPolicy.noCache,
errorPolicy: originalOptions.errorPolicy,
document: document,
@@ -34,7 +34,8 @@ Future<QueryResult> fetchMoreImplementation(
},
);
- QueryResult fetchMoreResult = await queryManager.query(combinedOptions);
+ QueryResult<TParsed> fetchMoreResult =
+ await queryManager.query(combinedOptions);
try {
// combine the query with the new query, using the function provided by the user

diff:
@@ -6,6 +6,7 @@ import 'package:graphql/src/core/observable_query.dart';
import 'package:gql/ast.dart';
import 'package:gql_exec/gql_exec.dart';
+import 'package:graphql/src/core/result_parser.dart';
import 'package:graphql/src/exceptions.dart';
import 'package:graphql/src/core/query_result.dart';
@@ -19,7 +20,7 @@ typedef OnMutationUpdate = FutureOr<void> Function(
);
typedef OnError = FutureOr<void> Function(OperationException? error);
-class MutationOptions extends BaseOptions {
+class MutationOptions<TParsed> extends BaseOptions<TParsed> {
MutationOptions({
required DocumentNode document,
String? operationName,
@@ -32,6 +33,7 @@ class MutationOptions extends BaseOptions {
this.onCompleted,
this.update,
this.onError,
+ ResultParserFn<TParsed>? parserFn,
}) : super(
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
@@ -41,6 +43,7 @@ class MutationOptions extends BaseOptions {
variables: variables,
context: context,
optimisticResult: optimisticResult,
+ parserFn: parserFn,
);
final OnMutationCompleted? onCompleted;

diff:
@@ -58,7 +58,7 @@ enum QueryLifecycle {
/// And a handful of internally leveraged methods.
///
/// [apollo_oq]: https://www.apollographql.com/docs/react/v3.0-beta/api/core/ObservableQuery/
-class ObservableQuery {
+class ObservableQuery<TParsed> {
ObservableQuery({
required this.queryManager,
required this.options,
@@ -67,7 +67,7 @@ class ObservableQuery {
_latestWasEagerlyFetched = true;
fetchResults();
}
- controller = StreamController<QueryResult>.broadcast(
+ controller = StreamController<QueryResult<TParsed>>.broadcast(
onListen: onListen,
);
}
@@ -94,15 +94,15 @@ class ObservableQuery {
queryManager.maybeRebroadcastQueries(exclude: this);
/// The most recently seen result from this operation's stream
- QueryResult? latestResult;
+ QueryResult<TParsed>? latestResult;
QueryLifecycle lifecycle = QueryLifecycle.unexecuted;
- WatchQueryOptions options;
+ WatchQueryOptions<TParsed> options;
- late StreamController<QueryResult> controller;
+ late StreamController<QueryResult<TParsed>> controller;
- Stream<QueryResult> get stream => controller.stream;
+ Stream<QueryResult<TParsed>> get stream => controller.stream;
bool get isCurrentlyPolling => lifecycle == QueryLifecycle.polling;
bool get isRefetchSafe {
@@ -128,10 +128,13 @@ class ObservableQuery {
///
/// **NOTE:** overrides any present non-network-only [FetchPolicy],
/// as refetching from the `cache` does not make sense.
- Future<QueryResult?> refetch() {
+ Future<QueryResult<TParsed>?> refetch() {
if (isRefetchSafe) {
- addResult(QueryResult.loading(data: latestResult?.data));
- return queryManager.refetchQuery(queryId);
+ addResult(QueryResult.loading(
+ data: latestResult?.data,
+ parserFn: options.parserFn,
+ ));
+ return queryManager.refetchQuery<TParsed>(queryId);
}
throw Exception('Query is not refetch safe');
}
@@ -179,8 +182,8 @@ class ObservableQuery {
/// Fetch results based on [options.fetchPolicy]
///
/// Will [startPolling] if [options.pollInterval] is set
- MultiSourceResult fetchResults() {
- final MultiSourceResult allResults =
+ MultiSourceResult<TParsed> fetchResults() {
+ final MultiSourceResult<TParsed> allResults =
queryManager.fetchQueryAsMultiSourceResult(queryId, options);
latestResult ??= allResults.eagerResult;
@@ -212,8 +215,12 @@ class ObservableQuery {
/// it is easy to make mistakes in writing [updateQuery].
///
/// To mitigate this, [FetchMoreOptions.partial] has been provided.
- Future<QueryResult> fetchMore(FetchMoreOptions fetchMoreOptions) async {
- addResult(QueryResult.loading(data: latestResult?.data));
+ Future<QueryResult<TParsed>> fetchMore(
+ FetchMoreOptions fetchMoreOptions) async {
+ addResult(QueryResult.loading(
+ data: latestResult?.data,
+ parserFn: options.parserFn,
+ ));
return fetchMoreImplementation(
fetchMoreOptions,
@@ -231,7 +238,7 @@ class ObservableQuery {
/// if it is set to `null`.
///
/// Called internally by the [QueryManager]
- void addResult(QueryResult result, {bool fromRebroadcast = false}) {
+ void addResult(QueryResult<TParsed> result, {bool fromRebroadcast = false}) {
// don't overwrite results due to some async/optimism issue
if (latestResult != null &&
latestResult!.timestamp.isAfter(result.timestamp)) {

diff:
import 'dart:async';
+import 'package:graphql/src/core/result_parser.dart';
import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
@@ -48,8 +49,9 @@ class QueryManager {
/// prevents rebroadcasting for some intensive bulk operation like [refetchSafeQueries]
bool rebroadcastLocked = false;
- ObservableQuery watchQuery(WatchQueryOptions options) {
- final ObservableQuery observableQuery = ObservableQuery(
+ ObservableQuery<TParsed> watchQuery<TParsed>(
+ WatchQueryOptions<TParsed> options) {
+ final ObservableQuery<TParsed> observableQuery = ObservableQuery<TParsed>(
queryManager: this,
options: options,
);
@@ -59,7 +61,8 @@ class QueryManager {
return observableQuery;
}
- Stream<QueryResult> subscribe(SubscriptionOptions options) async* {
+ Stream<QueryResult<TParsed>> subscribe<TParsed>(
+ SubscriptionOptions<TParsed> options) async* {
assert(
options.fetchPolicy != FetchPolicy.cacheOnly,
"Cannot subscribe with FetchPolicy.cacheOnly: $options",
@@ -70,7 +73,9 @@ class QueryManager {
if (options.optimisticResult != null) {
// TODO optimisticResults for streams just skip the cache for now
yield QueryResult.optimistic(
- data: options.optimisticResult as Map<String, dynamic>?);
+ data: options.optimisticResult as Map<String, dynamic>?,
+ parserFn: options.parserFn,
+ );
} else if (shouldRespondEagerlyFromCache(options.fetchPolicy)) {
final cacheResult = cache.readQuery(
request,
@@ -80,6 +85,7 @@ class QueryManager {
yield QueryResult(
source: QueryResultSource.cache,
data: cacheResult,
+ parserFn: options.parserFn,
);
}
}
@@ -103,7 +109,10 @@ class QueryManager {
);
} catch (failure, trace) {
// we set the source to indicate where the source of failure
- queryResult ??= QueryResult(source: QueryResultSource.network);
+ queryResult ??= QueryResult(
+ source: QueryResultSource.network,
+ parserFn: options.parserFn,
+ );
queryResult.exception = coalesceErrors(
exception: queryResult.exception,
@@ -118,21 +127,33 @@ class QueryManager {
return queryResult;
}).transform(StreamTransformer.fromHandlers(
- handleError: (err, trace, sink) => sink.add(_wrapFailure(err, trace)),
+ handleError: (err, trace, sink) => sink.add(_wrapFailure(
+ err,
+ trace,
+ options.parserFn,
+ )),
));
} catch (ex, trace) {
- yield* Stream.fromIterable([_wrapFailure(ex, trace)]);
+ yield* Stream.fromIterable([
+ _wrapFailure(
+ ex,
+ trace,
+ options.parserFn,
+ )
+ ]);
}
}
- Future<QueryResult> query(QueryOptions options) async {
+ Future<QueryResult<TParsed>> query<TParsed>(
+ QueryOptions<TParsed> options) async {
final result = await fetchQuery(_oneOffOpId, options);
maybeRebroadcastQueries();
return result;
}
- Future<QueryResult> mutate(MutationOptions options) async {
+ Future<QueryResult<TParsed>> mutate<TParsed>(
+ MutationOptions<TParsed> options) async {
final result = await fetchQuery(_oneOffOpId, options);
// once the mutation has been process successfully, execute callbacks
// before returning the results
@@ -154,25 +175,25 @@ class QueryManager {
return result;
}
- Future<QueryResult> fetchQuery(
+ Future<QueryResult<TParsed>> fetchQuery<TParsed>(
String queryId,
- BaseOptions options,
+ BaseOptions<TParsed> options,
) async {
- final MultiSourceResult allResults =
+ final MultiSourceResult<TParsed> allResults =
fetchQueryAsMultiSourceResult(queryId, options);
return allResults.networkResult ?? allResults.eagerResult;
}
/// Wrap both the `eagerResult` and `networkResult` future in a `MultiSourceResult`
/// if the cache policy precludes a network request, `networkResult` will be `null`
- MultiSourceResult fetchQueryAsMultiSourceResult(
+ MultiSourceResult<TParsed> fetchQueryAsMultiSourceResult<TParsed>(
String queryId,
- BaseOptions options,
+ BaseOptions<TParsed> options,
) {
// create a new request to execute
final request = options.asRequest;
- final QueryResult eagerResult = _resolveQueryEagerly(
+ final QueryResult<TParsed> eagerResult = _resolveQueryEagerly(
request,
queryId,
options,
@@ -181,6 +202,7 @@ class QueryManager {
// _resolveQueryEagerly handles cacheOnly,
// so if we're loading + cacheFirst we continue to network
return MultiSourceResult(
+ parserFn: options.parserFn,
eagerResult: eagerResult,
networkResult:
(shouldStopAtCache(options.fetchPolicy) && !eagerResult.isLoading)
@@ -191,13 +213,13 @@ class QueryManager {
/// Resolve the query on the network,
/// negotiating any necessary cache edits / optimistic cleanup
- Future<QueryResult> _resolveQueryOnNetwork(
+ Future<QueryResult<TParsed>> _resolveQueryOnNetwork<TParsed>(
Request request,
String queryId,
- BaseOptions options,
+ BaseOptions<TParsed> options,
) async {
Response response;
- QueryResult? queryResult;
+ QueryResult<TParsed>? queryResult;
bool rereadFromCache = false;
@@ -219,7 +241,10 @@ class QueryManager {
);
} catch (failure, trace) {
// we set the source to indicate where the source of failure
- queryResult ??= QueryResult(source: QueryResultSource.network);
+ queryResult ??= QueryResult(
+ source: QueryResultSource.network,
+ parserFn: options.parserFn,
+ );
queryResult.exception = coalesceErrors(
exception: queryResult.exception,
@@ -245,12 +270,14 @@ class QueryManager {
/// Add an eager cache response to the stream if possible,
/// based on `fetchPolicy` and `optimisticResults`
- QueryResult _resolveQueryEagerly(
+ QueryResult<TParsed> _resolveQueryEagerly<TParsed>(
Request request,
String queryId,
- BaseOptions options,
+ BaseOptions<TParsed> options,
) {
- QueryResult queryResult = QueryResult.loading();
+ QueryResult<TParsed> queryResult = QueryResult.loading(
+ parserFn: options.parserFn,
+ );
try {
if (options.optimisticResult != null) {
@@ -258,6 +285,7 @@ class QueryManager {
request,
queryId: queryId,
optimisticResult: options.optimisticResult,
+ options: options,
);
}
@@ -271,6 +299,7 @@ class QueryManager {
queryResult = QueryResult(
data: data,
source: QueryResultSource.cache,
+ parserFn: options.parserFn,
);
}
@@ -278,6 +307,7 @@ class QueryManager {
queryResult.isLoading) {
queryResult = QueryResult(
source: QueryResultSource.cache,
+ parserFn: options.parserFn,
exception: OperationException(
linkException: CacheMissException(
'Could not resolve the given request against the cache. (FetchPolicy.cacheOnly)',
@@ -312,8 +342,9 @@ class QueryManager {
/// Refetch the [ObservableQuery] referenced by [queryId],
/// overriding any present non-network-only [FetchPolicy].
- Future<QueryResult?> refetchQuery(String queryId) {
- final WatchQueryOptions options = queries[queryId]!.options.copy();
+ Future<QueryResult<TParsed>?> refetchQuery<TParsed>(String queryId) {
+ final WatchQueryOptions<TParsed> options =
+ queries[queryId]!.options.copy() as WatchQueryOptions<TParsed>;
if (!willAlwaysExecuteOnNetwork(options.fetchPolicy)) {
options.policies = options.policies.copyWith(
fetch: FetchPolicy.networkOnly,
@@ -350,12 +381,13 @@ class QueryManager {
/// Will [maybeRebroadcastQueries] from [ObservableQuery.addResult] if the [cache] has flagged the need to.
///
/// Queries are registered via [setQuery] and [watchQuery]
- void addQueryResult(
+ void addQueryResult<TParsed>(
Request request,
String? queryId,
- QueryResult queryResult,
+ QueryResult<TParsed> queryResult,
) {
- final ObservableQuery? observableQuery = getQuery(queryId);
+ final ObservableQuery<TParsed>? observableQuery =
+ getQuery(queryId) as ObservableQuery<TParsed>?;
if (observableQuery != null && !observableQuery.controller.isClosed) {
observableQuery.addResult(queryResult);
@@ -363,13 +395,15 @@ class QueryManager {
}
/// Create an optimstic result for the query specified by `queryId`, if it exists
- QueryResult _getOptimisticQueryResult(
+ QueryResult<TParsed> _getOptimisticQueryResult<TParsed>(
Request request, {
required String queryId,
required Object? optimisticResult,
+ required BaseOptions<TParsed> options,
}) {
- QueryResult queryResult = QueryResult(
+ QueryResult<TParsed> queryResult = QueryResult(
source: QueryResultSource.optimisticResult,
+ parserFn: options.parserFn,
);
attemptCacheWriteFromClient(
@@ -462,9 +496,9 @@ class QueryManager {
return requestId;
}
- QueryResult mapFetchResultToQueryResult(
+ QueryResult<TParsed> mapFetchResultToQueryResult<TParsed>(
Response response,
- BaseOptions options, {
+ BaseOptions<TParsed> options, {
required QueryResultSource source,
}) {
List<GraphQLError>? errors;
@@ -499,12 +533,19 @@ class QueryManager {
context: response.context,
source: source,
exception: coalesceErrors(graphqlErrors: errors),
+ parserFn: options.parserFn,
);
}
}
-QueryResult _wrapFailure(dynamic ex, trace) => QueryResult(
+QueryResult<TParsed> _wrapFailure<TParsed>(
+ dynamic ex,
+ trace,
+ ResultParserFn<TParsed> parserFn,
+) =>
+ QueryResult(
// we set the source to indicate where the source of failure
source: QueryResultSource.network,
exception: coalesceErrors(linkException: translateFailure(ex, trace)),
+ parserFn: parserFn,
);

diff:
// ignore_for_file: deprecated_member_use_from_same_package
+import 'package:gql/language.dart';
import 'package:graphql/src/core/_base_options.dart';
+import 'package:graphql/src/core/result_parser.dart';
import 'package:graphql/src/utilities/helpers.dart';
import 'package:gql/ast.dart';
@@ -7,7 +9,7 @@ import 'package:gql/ast.dart';
import 'package:graphql/client.dart';
/// Query options.
-class QueryOptions extends BaseOptions {
+class QueryOptions<TParsed> extends BaseOptions<TParsed> {
QueryOptions({
required DocumentNode document,
String? operationName,
@@ -18,6 +20,7 @@ class QueryOptions extends BaseOptions {
Object? optimisticResult,
this.pollInterval,
Context? context,
+ ResultParserFn<TParsed>? parserFn,
}) : super(
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
@@ -27,6 +30,7 @@ class QueryOptions extends BaseOptions {
variables: variables,
context: context,
optimisticResult: optimisticResult,
+ parserFn: parserFn,
);
/// The time interval on which this query should be re-fetched from the server.
@@ -35,7 +39,7 @@ class QueryOptions extends BaseOptions {
@override
List<Object?> get properties => [...super.properties, pollInterval];
- WatchQueryOptions asWatchQueryOptions({bool fetchResults = true}) =>
+ WatchQueryOptions<TParsed> asWatchQueryOptions({bool fetchResults = true}) =>
WatchQueryOptions(
document: document,
operationName: operationName,
@@ -47,10 +51,11 @@ class QueryOptions extends BaseOptions {
fetchResults: fetchResults,
context: context,
optimisticResult: optimisticResult,
+ parserFn: this.parserFn,
);
}
-class SubscriptionOptions extends BaseOptions {
+class SubscriptionOptions<TParsed> extends BaseOptions<TParsed> {
SubscriptionOptions({
required DocumentNode document,
String? operationName,
@@ -60,6 +65,7 @@ class SubscriptionOptions extends BaseOptions {
CacheRereadPolicy? cacheRereadPolicy,
Object? optimisticResult,
Context? context,
+ ResultParserFn<TParsed>? parserFn,
}) : super(
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
@@ -69,13 +75,14 @@ class SubscriptionOptions extends BaseOptions {
variables: variables,
context: context,
optimisticResult: optimisticResult,
+ parserFn: parserFn,
);
/// An optimistic first result to eagerly add to the subscription stream
Object? optimisticResult;
}
-class WatchQueryOptions extends QueryOptions {
+class WatchQueryOptions<TParsed> extends QueryOptions<TParsed> {
WatchQueryOptions({
required DocumentNode document,
String? operationName,
@@ -89,6 +96,7 @@ class WatchQueryOptions extends QueryOptions {
this.carryForwardDataOnException = true,
bool? eagerlyFetchResults,
Context? context,
+ ResultParserFn<TParsed>? parserFn,
}) : eagerlyFetchResults = eagerlyFetchResults ?? fetchResults,
super(
document: document,
@@ -100,6 +108,7 @@ class WatchQueryOptions extends QueryOptions {
pollInterval: pollInterval,
context: context,
optimisticResult: optimisticResult,
+ parserFn: parserFn,
);
/// Whether or not to fetch results
@@ -117,7 +126,7 @@ class WatchQueryOptions extends QueryOptions {
List<Object?> get properties =>
[...super.properties, fetchResults, eagerlyFetchResults];
- WatchQueryOptions copy() => WatchQueryOptions(
+ WatchQueryOptions<TParsed> copy() => WatchQueryOptions<TParsed>(
document: document,
operationName: operationName,
variables: variables,
@@ -130,6 +139,7 @@ class WatchQueryOptions extends QueryOptions {
eagerlyFetchResults: eagerlyFetchResults,
carryForwardDataOnException: carryForwardDataOnException,
context: context,
+ parserFn: parserFn,
);
}

diff:
import 'dart:async' show FutureOr;
import 'package:graphql/client.dart';
+import 'package:graphql/src/core/result_parser.dart';
/// The source of the result data contained
///
@@ -37,33 +38,41 @@ final _eagerSources = {
};
/// A single operation result
-class QueryResult {
+class QueryResult<TParsed> {
QueryResult({
this.data,
this.exception,
this.context = const Context(),
+ required this.parserFn,
required this.source,
}) : timestamp = DateTime.now();
/// Unexecuted singleton, used as a placeholder for mutations,
/// etc.
- static final unexecuted = QueryResult(source: null)
- ..timestamp = DateTime.fromMillisecondsSinceEpoch(0);
+ static final unexecuted = QueryResult(
+ source: null,
+ parserFn: (d) => throw new UnimplementedError(
+ "Unexecuted query data can not be parsed."),
+ )..timestamp = DateTime.fromMillisecondsSinceEpoch(0);
factory QueryResult.loading({
Map<String, dynamic>? data,
+ required ResultParserFn<TParsed> parserFn,
}) =>
QueryResult(
data: data,
source: QueryResultSource.loading,
+ parserFn: parserFn,
);
factory QueryResult.optimistic({
Map<String, dynamic>? data,
+ required ResultParserFn<TParsed> parserFn,
}) =>
QueryResult(
data: data,
source: QueryResultSource.optimisticResult,
+ parserFn: parserFn,
);
DateTime timestamp;
@@ -82,6 +91,8 @@ class QueryResult {
OperationException? exception;
+ ResultParserFn<TParsed> parserFn;
+
/// [data] has yet to be specified from any source
/// for the _most recent_ operation
/// (including [QueryResultSource.optimisticResult])
@@ -107,6 +118,17 @@ class QueryResult {
/// Whether the response includes an [exception]
bool get hasException => (exception != null);
+ /// If a parserFn is provided, this getter can be used to fetch the parsed data.
+ TParsed? get parsedData {
+ final data = this.data;
+ final parserFn = this.parserFn;
+
+ if (data == null) {
+ return null;
+ }
+ return parserFn(data);
+ }
+
@override
String toString() => 'QueryResult('
'source: $source, '
@@ -117,16 +139,17 @@ class QueryResult {
')';
}
-class MultiSourceResult {
+class MultiSourceResult<TParsed> {
MultiSourceResult({
- QueryResult? eagerResult,
+ QueryResult<TParsed>? eagerResult,
this.networkResult,
- }) : eagerResult = eagerResult ?? QueryResult.loading(),
+ required ResultParserFn<TParsed> parserFn,
+ }) : eagerResult = eagerResult ?? QueryResult.loading(parserFn: parserFn),
assert(
eagerResult!.source != QueryResultSource.network,
'An eager result cannot be gotten from the network',
);
- QueryResult eagerResult;
- FutureOr<QueryResult>? networkResult;
+ QueryResult<TParsed> eagerResult;
+ FutureOr<QueryResult<TParsed>>? networkResult;
}

diff: +typedef ResultParserFn<TResult> = TResult Function(Map<String, dynamic> data);

diff:
@@ -87,7 +87,8 @@ class GraphQLClient implements GraphQLDataProxy {
/// observableQuery.close();
/// ```
/// {@end-tool}
- ObservableQuery watchQuery(WatchQueryOptions options) {
+ ObservableQuery<TParsed> watchQuery<TParsed>(
+ WatchQueryOptions<TParsed> options) {
options.policies =
defaultPolicies.watchQuery.withOverrides(options.policies);
return queryManager.watchQuery(options);
@@ -98,7 +99,8 @@ class GraphQLClient implements GraphQLDataProxy {
/// This is a stop-gap solution to the problems created by the reliance of `graphql_flutter` on [ObservableQuery] for mutations.
///
/// For more details, see https://github.com/zino-app/graphql-flutter/issues/774
- ObservableQuery watchMutation(WatchQueryOptions options) {
+ ObservableQuery<TParsed> watchMutation<TParsed>(
+ WatchQueryOptions<TParsed> options) {
options.policies =
defaultPolicies.watchMutation.withOverrides(options.policies);
return queryManager.watchQuery(options);
@@ -144,16 +146,19 @@ class GraphQLClient implements GraphQLDataProxy {
/// ```
/// {@end-tool}
- Future<QueryResult> query(QueryOptions options) {
+ Future<QueryResult<TParsed>> query<TParsed>(
+ QueryOptions<TParsed> options,
+ ) async {
options.policies = defaultPolicies.query.withOverrides(options.policies);
- return queryManager.query(options);
+ return await queryManager.query(options);
}
/// This resolves a single mutation according to the [MutationOptions] specified and
/// returns a [Future] which resolves with the [QueryResult] or throws an [Exception].
- Future<QueryResult> mutate(MutationOptions options) {
+ Future<QueryResult<TParsed>> mutate<TParsed>(
+ MutationOptions<TParsed> options) async {
options.policies = defaultPolicies.mutate.withOverrides(options.policies);
- return queryManager.mutate(options);
+ return await queryManager.mutate(options);
}
/// This subscribes to a GraphQL subscription according to the options specified and returns a
@@ -192,7 +197,8 @@ class GraphQLClient implements GraphQLDataProxy {
/// });
/// ```
/// {@end-tool}
- Stream<QueryResult> subscribe(SubscriptionOptions options) {
+ Stream<QueryResult<TParsed>> subscribe<TParsed>(
+ SubscriptionOptions<TParsed> options) {
options.policies = defaultPolicies.subscribe.withOverrides(
options.policies,
);
@@ -207,17 +213,18 @@ class GraphQLClient implements GraphQLDataProxy {
///
/// To mitigate this, [FetchMoreOptions.partial] has been provided.
@experimental
- Future<QueryResult> fetchMore(
+ Future<QueryResult<TParsed>> fetchMore<TParsed>(
FetchMoreOptions fetchMoreOptions, {
- required QueryOptions originalOptions,
- required QueryResult previousResult,
- }) =>
- fetchMoreImplementation(
+ required QueryOptions<TParsed> originalOptions,
+ required QueryResult<TParsed> previousResult,
+ }) async {
+ return await fetchMoreImplementation(
fetchMoreOptions,
originalOptions: originalOptions,
previousResult: previousResult,
queryManager: queryManager,
);
+ }
/// pass through to [cache.readQuery]
readQuery(request, {optimistic = true}) =>

diff:
@@ -4,40 +4,40 @@ import 'package:graphql/client.dart';
import 'package:graphql_flutter/src/widgets/graphql_provider.dart';
-typedef RunMutation = MultiSourceResult Function(
+typedef RunMutation<TParsed> = MultiSourceResult<TParsed> Function(
Map<String, dynamic> variables, {
Object? optimisticResult,
});
-typedef MutationBuilder = Widget Function(
+typedef MutationBuilder<TParsed> = Widget Function(
RunMutation runMutation,
- QueryResult? result,
+ QueryResult<TParsed>? result,
);
/// Builds a [Mutation] widget based on the a given set of [MutationOptions]
/// that streams [QueryResult]s into the [QueryBuilder].
-class Mutation extends StatefulWidget {
+class Mutation<TParsed> extends StatefulWidget {
const Mutation({
final Key? key,
required this.options,
required this.builder,
}) : super(key: key);
- final MutationOptions options;
- final MutationBuilder builder;
+ final MutationOptions<TParsed> options;
+ final MutationBuilder<TParsed> builder;
@override
- MutationState createState() => MutationState();
+ MutationState<TParsed> createState() => MutationState();
}
-class MutationState extends State<Mutation> {
+class MutationState<TParsed> extends State<Mutation<TParsed>> {
GraphQLClient? client;
- ObservableQuery? observableQuery;
+ ObservableQuery<TParsed>? observableQuery;
- WatchQueryOptions? __cachedOptions;
+ WatchQueryOptions<TParsed>? __cachedOptions;
- WatchQueryOptions get _providedOptions {
- final _options = WatchQueryOptions(
+ WatchQueryOptions<TParsed> get _providedOptions {
+ final _options = WatchQueryOptions<TParsed>(
document: widget.options.document,
operationName: widget.options.operationName,
variables: widget.options.variables,
@@ -46,6 +46,7 @@ class MutationState extends State<Mutation> {
cacheRereadPolicy: widget.options.cacheRereadPolicy,
fetchResults: false,
context: widget.options.context,
+ parserFn: widget.options.parserFn,
);
__cachedOptions ??= _options;
return _options;
@@ -79,7 +80,7 @@ class MutationState extends State<Mutation> {
}
@override
- void didUpdateWidget(Mutation oldWidget) {
+ void didUpdateWidget(Mutation<TParsed> oldWidget) {
super.didUpdateWidget(oldWidget);
// TODO @micimize - investigate why/if this was causing issues
@@ -90,7 +91,7 @@ class MutationState extends State<Mutation> {
/// Run the mutation with the given `variables` and `optimisticResult`,
/// returning a [MultiSourceResult] for handling both the eager and network results
- MultiSourceResult runMutation(
+ MultiSourceResult<TParsed> runMutation(
Map<String, dynamic> variables, {
Object? optimisticResult,
}) {
@@ -116,12 +117,12 @@ class MutationState extends State<Mutation> {
@override
Widget build(BuildContext context) {
- return StreamBuilder<QueryResult?>(
+ return StreamBuilder<QueryResult<TParsed>?>(
initialData: observableQuery?.latestResult ?? QueryResult.unexecuted,
stream: observableQuery?.stream,
builder: (
BuildContext buildContext,
- AsyncSnapshot<QueryResult?> snapshot,
+ AsyncSnapshot<QueryResult<TParsed>?> snapshot,
) {
return widget.builder(
runMutation,

diff:
@@ -5,37 +5,39 @@ import 'package:graphql/client.dart';
import 'package:graphql_flutter/src/widgets/graphql_provider.dart';
// method to call from widget to fetchmore queries
-typedef FetchMore = Future<QueryResult> Function(FetchMoreOptions options);
+typedef FetchMore<TParsed> = Future<QueryResult<TParsed>> Function(
+ FetchMoreOptions options);
-typedef Refetch = Future<QueryResult?> Function();
+typedef Refetch<TParsed> = Future<QueryResult<TParsed>?> Function();
-typedef QueryBuilder = Widget Function(
- QueryResult result, {
- Refetch? refetch,
- FetchMore? fetchMore,
+typedef QueryBuilder<TParsed> = Widget Function(
+ QueryResult<TParsed> result, {
+ Refetch<TParsed>? refetch,
+ FetchMore<TParsed>? fetchMore,
});
/// Builds a [Query] widget based on the a given set of [QueryOptions]
/// that streams [QueryResult]s into the [QueryBuilder].
-class Query extends StatefulWidget {
+class Query<TParsed> extends StatefulWidget {
const Query({
final Key? key,
required this.options,
required this.builder,
}) : super(key: key);
- final QueryOptions options;
- final QueryBuilder builder;
+ final QueryOptions<TParsed> options;
+ final QueryBuilder<TParsed> builder;
@override
- QueryState createState() => QueryState();
+ QueryState<TParsed> createState() => QueryState();
}
-class QueryState extends State<Query> {
- ObservableQuery? observableQuery;
+class QueryState<TParsed> extends State<Query<TParsed>> {
+ ObservableQuery<TParsed>? observableQuery;
GraphQLClient? _client;
- WatchQueryOptions get _options => widget.options.asWatchQueryOptions();
+ WatchQueryOptions<TParsed> get _options =>
+ widget.options.asWatchQueryOptions();
void _initQuery() {
observableQuery?.close();
@@ -53,7 +55,7 @@ class QueryState extends State<Query> {
}
@override
- void didUpdateWidget(Query oldWidget) {
+ void didUpdateWidget(Query<TParsed> oldWidget) {
super.didUpdateWidget(oldWidget);
final GraphQLClient client = GraphQLProvider.of(context).value;
@@ -75,12 +77,15 @@ class QueryState extends State<Query> {
@override
Widget build(BuildContext context) {
- return StreamBuilder<QueryResult>(
- initialData: observableQuery?.latestResult ?? QueryResult.loading(),
+ return StreamBuilder<QueryResult<TParsed>>(
+ initialData: observableQuery?.latestResult ??
+ QueryResult.loading(
+ parserFn: widget.options.parserFn,
+ ),
stream: observableQuery!.stream,
builder: (
BuildContext buildContext,
- AsyncSnapshot<QueryResult> snapshot,
+ AsyncSnapshot<QueryResult<TParsed>> snapshot,
) {
return widget.builder(
snapshot.data!,

diff:
@@ -5,12 +5,13 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
-typedef OnSubscriptionResult = void Function(
- QueryResult subscriptionResult,
+typedef OnSubscriptionResult<TParsed> = void Function(
+ QueryResult<TParsed> subscriptionResult,
GraphQLClient? client,
);
-typedef SubscriptionBuilder = Widget Function(QueryResult result);
+typedef SubscriptionBuilder<TParsed> = Widget Function(
+ QueryResult<TParsed> result);
/// Creats a subscription with [GraphQLClient.subscribe].
///
@@ -62,7 +63,7 @@ typedef SubscriptionBuilder = Widget Function(QueryResult result);
/// }
/// ```
/// {@end-tool}
-class Subscription extends StatefulWidget {
+class Subscription<TParsed> extends StatefulWidget {
const Subscription({
required this.options,
required this.builder,
@@ -70,16 +71,16 @@ class Subscription extends StatefulWidget {
Key? key,
}) : super(key: key);
- final SubscriptionOptions options;
- final SubscriptionBuilder builder;
- final OnSubscriptionResult? onSubscriptionResult;
+ final SubscriptionOptions<TParsed> options;
+ final SubscriptionBuilder<TParsed> builder;
+ final OnSubscriptionResult<TParsed>? onSubscriptionResult;
@override
- _SubscriptionState createState() => _SubscriptionState();
+ _SubscriptionState<TParsed> createState() => _SubscriptionState();
}
-class _SubscriptionState extends State<Subscription> {
- Stream<QueryResult>? stream;
+class _SubscriptionState<TParsed> extends State<Subscription<TParsed>> {
+ Stream<QueryResult<TParsed>>? stream;
GraphQLClient? client;
ConnectivityResult? _currentConnectivityResult;
@@ -115,7 +116,7 @@ class _SubscriptionState extends State<Subscription> {
}
@override
- void didUpdateWidget(Subscription oldWidget) {
+ void didUpdateWidget(Subscription<TParsed> oldWidget) {
super.didUpdateWidget(oldWidget);
if (!widget.options.equal(oldWidget.options)) {
@@ -159,16 +160,17 @@ class _SubscriptionState extends State<Subscription> {
@override
Widget build(BuildContext context) {
- return StreamBuilder<QueryResult>(
+ return StreamBuilder<QueryResult<TParsed>>(
initialData: widget.options.optimisticResult != null
? QueryResult.optimistic(
data: widget.options.optimisticResult as Map<String, dynamic>?,
+ parserFn: widget.options.parserFn,
)
- : QueryResult.loading(),
+ : QueryResult.loading(parserFn: widget.options.parserFn),
stream: stream,
builder: (
BuildContext buildContext,
- AsyncSnapshot<QueryResult> snapshot,
+ AsyncSnapshot<QueryResult<TParsed>> snapshot,
) {
return widget.builder(snapshot.data!);
},

original_message: feat: Add parser support


message: feat: Add tests

diff:
@@ -92,7 +92,7 @@ class QueryManager {
try {
yield* link.request(request).map((response) {
- QueryResult? queryResult;
+ QueryResult<TParsed>? queryResult;
bool rereadFromCache = false;
try {
queryResult = mapFetchResultToQueryResult(

diff:
+import 'package:graphql/src/core/result_parser.dart';
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
@@ -41,7 +42,7 @@ void main() {
}
}
''';
- readRepositoryData({
+ Map<String, dynamic> readRepositoryData({
bool withTypenames = true,
bool withIds = true,
bool viewerHasStarred = false,
@@ -152,6 +153,75 @@ void main() {
equals('bar'),
);
});
+ test('successful response with parser', () async {
+ final ResultParserFn<List<String>> parserFn = (data) {
+ return data['viewer']['repositories']['nodes']
+ .map<String>((node) => node['name'] as String)
+ .toList();
+ };
+ final _options = QueryOptions(
+ document: parseString(readRepositories),
+ variables: <String, dynamic>{
+ 'nRepositories': 42,
+ },
+ parserFn: parserFn,
+ );
+ final repoData = readRepositoryData(withTypenames: true);
+
+ when(
+ link.request(any),
+ ).thenAnswer(
+ (_) => Stream.fromIterable([
+ Response(
+ data: repoData,
+ context: Context().withEntry(
+ HttpLinkResponseContext(
+ statusCode: 200,
+ headers: {'foo': 'bar'},
+ ),
+ ),
+ ),
+ ]),
+ );
+
+ final QueryResult<List<String>> r = await client.query(_options);
+
+ verify(
+ link.request(
+ Request(
+ operation: Operation(
+ document: parseString(readRepositories),
+ //operationName: 'ReadRepositories',
+ ),
+ variables: <String, dynamic>{
+ 'nRepositories': 42,
+ },
+ context: Context(),
+ ),
+ ),
+ );
+
+ expect(r.exception, isNull);
+ expect(r.data, equals(repoData));
+
+ List<String>? parsedData = r.parsedData;
+ expect(
+ parsedData,
+ equals([
+ 'pq',
+ 'go-evercookie',
+ 'watchbot',
+ ]));
+
+ expect(
+ r.context.entry<HttpLinkResponseContext>()!.statusCode,
+ equals(200),
+ );
+ expect(
+ r.context.entry<HttpLinkResponseContext>()!.headers['foo'],
+ equals('bar'),
+ );
+ });
test('successful response without normalization', () async {
final readUnidentifiedRepositories = parseString(r'''
@@ -427,6 +497,54 @@ void main() {
response.data!['action']['starrable']['viewerHasStarred'] as bool?;
expect(viewerHasStarred, true);
});
+ test('successful mutation with parser', () async {
+ final ResultParserFn<bool> resultParser =
+ (data) => data['action']['starrable']['viewerHasStarred'] as bool;
+ final MutationOptions _options = MutationOptions(
+ document: parseString(addStar),
+ parserFn: resultParser,
+ );
+
+ when(
+ link.request(any),
+ ).thenAnswer(
+ (_) => Stream.fromIterable(
+ [
+ Response(
+ data: <String, dynamic>{
+ 'action': {
+ 'starrable': {
+ 'viewerHasStarred': true,
+ },
+ },
+ },
+ ),
+ ],
+ ),
+ );
+
+ final QueryResult response = await client.mutate(_options);
+
+ verify(
+ link.request(
+ Request(
+ operation: Operation(
+ document: parseString(addStar),
+ //operationName: 'AddStar',
+ ),
+ variables: <String, dynamic>{},
+ context: Context(),
+ ),
+ ),
+ );
+ final bool parsedResult = response.parsedData;
+ expect(parsedResult, isTrue);
+ expect(response.exception, isNull);
+ expect(response.data, isNotNull);
+ final bool? viewerHasStarred =
+ response.data!['action']['starrable']['viewerHasStarred'] as bool?;
+ expect(viewerHasStarred, true);
+ });
test('successful mutation through watchQuery', () async {
final _options = MutationOptions(
@@ -539,6 +657,59 @@ void main() {
),
);
});
+ test('parses results', () async {
+ final responses = [
+ {
+ 'id': '1',
+ 'name': 'first',
+ },
+ {
+ 'id': '2',
+ 'name': 'second',
+ },
+ ].map((item) => Response(
+ data: <String, dynamic>{
+ 'item': {
+ '__typename': 'Item',
+ ...item,
+ },
+ },
+ ));
+ when(
+ link.request(any),
+ ).thenAnswer(
+ (_) => Stream.fromIterable(responses),
+ );
+
+ final ResultParserFn<String> parserFn =
+ (data) => data['item']['name'] as String;
+ ;
+
+ final stream = client.subscribe(
+ SubscriptionOptions(
+ parserFn: parserFn,
+ document: parseString(
+ r'''
+ subscription {
+ item {
+ id
+ name
+ }
+ }
+ ''',
+ ),
+ ),
+ );
+
+ expect(
+ stream,
+ emitsInOrder(['first', 'second']
+ .map((e) => isA<QueryResult<String>>().having((result) {
+ final String? parsed = result.parsedData;
+ return parsed;
+ }, "Parsed item", e))),
+ );
+ });
test('wraps stream exceptions', () async {
final ex = ServerException(

original_message: feat: Add tests


message:
fix: Pass parse type to run
I forgot to parse the `TParsed` to the runner in the `MutationBuilder`.

diff:
@@ -10,7 +10,7 @@ typedef RunMutation<TParsed> = MultiSourceResult<TParsed> Function(
});
typedef MutationBuilder<TParsed> = Widget Function(
- RunMutation runMutation,
+ RunMutation<TParsed> runMutation,
QueryResult<TParsed>? result,
);


message:
fix: Read from cache first on cacheAndNetwork
When fetch policy is cacheAndNetwork, we now return cache and then
fetch from network, if the cached result is available). If not
we fetch from network.

diff:
@@ -146,11 +146,22 @@ class QueryManager {
Future<QueryResult<TParsed>> query<TParsed>(
QueryOptions<TParsed> options) async {
- final result = await fetchQuery(_oneOffOpId, options);
+ final results = fetchQueryAsMultiSourceResult(_oneOffOpId, options);
+ final eagerResult = results.eagerResult;
+ final networkResult = results.networkResult;
+ if (options.fetchPolicy != FetchPolicy.cacheAndNetwork ||
+ eagerResult.isLoading) {
+ final result = networkResult ?? eagerResult;
+ await result;
maybeRebroadcastQueries();
-
return result;
}
+ maybeRebroadcastQueries();
+ if (networkResult is Future<QueryResult<TParsed>>) {
+ networkResult.then((value) => maybeRebroadcastQueries());
+ }
+ return eagerResult;
+ }
Future<QueryResult<TParsed>> mutate<TParsed>(
MutationOptions<TParsed> options) async {

diff:
@@ -65,10 +65,10 @@ void main() {
group('query', () {
// TODO cacheFirst code path: Return result from cache. Only fetch from network if cached result is not available.
- // TODO cacheAndNetwork code path: Return result from cache first (if it exists), then return network result once it's available.
// TODO cacheOnly code path: Return result from cache if available, fail otherwise.
// TODO noCache code path: Return result from network, fail if network call doesn't succeed, don't save to cache.
// TODO networkOnly code path: Return result from network, fail if network call doesn't succeed, save to cache.
+
test('switch to cacheOnly returns cached data', () async {
final _options = QueryOptions(
fetchPolicy: FetchPolicy.cacheAndNetwork,
@@ -117,6 +117,50 @@ void main() {
expect(cacheResult.exception, isNull);
expect(cacheResult.data, equals(repoData));
});
+ test('cacheAndNetwork returns from cache (if exists)', () async {
+ final _options = QueryOptions(
+ fetchPolicy: FetchPolicy.cacheAndNetwork,
+ document: parseString(readRepositories),
+ variables: <String, dynamic>{
+ 'nRepositories': 42,
+ },
+ );
+ final repoData = readRepositoryData(withTypenames: true);
+
+ when(
+ link.request(any),
+ ).thenAnswer(
+ (_) => Stream.fromIterable([
+ Response(data: repoData),
+ ]),
+ );
+
+ final QueryResult r = await client.query(_options);
+
+ verify(
+ link.request(
+ Request(
+ operation: Operation(
+ document: parseString(readRepositories),
+ //operationName: 'ReadRepositories',
+ ),
+ variables: <String, dynamic>{
+ 'nRepositories': 42,
+ },
+ context: Context(),
+ ),
+ ),
+ );
+
+ expect(r.exception, isNull);
+ expect(r.data, equals(repoData));
+ expect(r.source, QueryResultSource.network);
+
+ final QueryResult cacheResult = await client.query(_options);
+ expect(cacheResult.exception, isNull);
+ expect(cacheResult.data, equals(repoData));
+ expect(cacheResult.source, equals(QueryResultSource.cache));
+ });
});
});
}


message: feat: Update options

diff:
-import 'package:graphql/src/core/_data_class.dart';
-
+import 'package:collection/collection.dart';
import 'package:gql/ast.dart';
import 'package:graphql/client.dart';
import 'package:graphql/src/core/result_parser.dart';
+import 'package:meta/meta.dart';
+
+TParsed unprovidedParserFn<TParsed>(_d) => throw UnimplementedError(
+ "Please provide a parser function to support result parsing.",
+ );
/// TODO refactor into [Request] container
/// Base options.
-abstract class BaseOptions<TParsed> extends MutableDataClass {
+@immutable
+abstract class BaseOptions<TParsed> {
BaseOptions({
required this.document,
this.variables = const {},
@@ -24,28 +29,25 @@ abstract class BaseOptions<TParsed> extends MutableDataClass {
cacheReread: cacheRereadPolicy,
),
context = context ?? Context(),
- parserFn = parserFn ??
- ((d) => throw UnimplementedError(
- "Please provide a parser function to support result parsing.",
- ));
+ parserFn = parserFn ?? unprovidedParserFn;
/// Document containing at least one [OperationDefinitionNode]
- DocumentNode document;
+ final DocumentNode document;
/// Name of the executable definition
///
/// Must be specified if [document] contains more than one [OperationDefinitionNode]
- String? operationName;
+ final String? operationName;
/// A map going from variable name to variable value, where the variables are used
/// within the GraphQL query.
- Map<String, dynamic> variables;
+ final Map<String, dynamic> variables;
/// An optimistic result to eagerly add to the operation stream
- Object? optimisticResult;
+ final Object? optimisticResult;
/// Specifies the [Policies] to be used during execution.
- Policies policies;
+ final Policies policies;
FetchPolicy? get fetchPolicy => policies.fetch;
@@ -54,9 +56,9 @@ abstract class BaseOptions<TParsed> extends MutableDataClass {
CacheRereadPolicy? get cacheRereadPolicy => policies.cacheReread;
/// Context to be passed to link execution chain.
- Context context;
+ final Context context;
- ResultParserFn<TParsed> parserFn;
+ final ResultParserFn<TParsed> parserFn;
// TODO consider inverting this relationship
/// Resolve these options into a request
@@ -69,7 +71,7 @@ abstract class BaseOptions<TParsed> extends MutableDataClass {
context: context,
);
- @override
+ @protected
List<Object?> get properties => [
document,
operationName,
@@ -77,6 +79,7 @@ abstract class BaseOptions<TParsed> extends MutableDataClass {
optimisticResult,
policies,
context,
+ parserFn,
];
OperationType get type {
@@ -95,4 +98,21 @@ abstract class BaseOptions<TParsed> extends MutableDataClass {
bool get isQuery => type == OperationType.query;
bool get isMutation => type == OperationType.mutation;
bool get isSubscription => type == OperationType.subscription;
+
+ /// [properties] based deep equality check
+ operator ==(Object other) =>
+ identical(this, other) ||
+ (other is BaseOptions &&
+ runtimeType == other.runtimeType &&
+ const ListEquality<Object?>(
+ DeepCollectionEquality(),
+ ).equals(
+ other.properties,
+ properties,
+ ));
+
+ @override
+ int get hashCode => const ListEquality<Object?>(
+ DeepCollectionEquality(),
+ ).hash(properties);
}

change_type: DELETE

diff:
-import 'package:meta/meta.dart';
-import "package:collection/collection.dart";
-
-/// 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();
-
- /// identifying properties for the inheriting class
- @protected
- List<Object?> get properties;
-
- /// [properties] based deep equality check
- bool equal(MutableDataClass other) =>
- identical(this, other) ||
- (runtimeType == other.runtimeType &&
- const ListEquality<Object?>(
- DeepCollectionEquality(),
- ).equals(
- other.properties,
- properties,
- ));
-}

diff:
-// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
-import 'package:graphql/src/cache/cache.dart';
+import 'package:graphql/client.dart';
import 'package:graphql/src/core/_base_options.dart';
-import 'package:graphql/src/core/observable_query.dart';
import 'package:gql/ast.dart';
-import 'package:gql_exec/gql_exec.dart';
import 'package:graphql/src/core/result_parser.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';
+import 'package:meta/meta.dart';
typedef OnMutationCompleted = FutureOr<void> Function(dynamic data);
typedef OnMutationUpdate = FutureOr<void> Function(
@@ -20,6 +15,7 @@ typedef OnMutationUpdate = FutureOr<void> Function(
);
typedef OnError = FutureOr<void> Function(OperationException? error);
+@immutable
class MutationOptions<TParsed> extends BaseOptions<TParsed> {
MutationOptions({
required DocumentNode document,
@@ -51,8 +47,41 @@ class MutationOptions<TParsed> extends BaseOptions<TParsed> {
final OnError? onError;
@override
- List<Object?> get properties =>
- [...super.properties, onCompleted, update, onError];
+ List<Object?> get properties => [
+ ...super.properties,
+ onCompleted,
+ update,
+ onError,
+ ];
+
+ MutationOptions<TParsed> copyWithPolicies(Policies policies) =>
+ MutationOptions(
+ document: document,
+ operationName: operationName,
+ variables: variables,
+ fetchPolicy: policies.fetch,
+ errorPolicy: policies.error,
+ cacheRereadPolicy: policies.cacheReread,
+ context: context,
+ optimisticResult: optimisticResult,
+ onCompleted: onCompleted,
+ update: update,
+ onError: onError,
+ parserFn: parserFn,
+ );
+
+ WatchQueryOptions<TParsed> asWatchQueryOptions() =>
+ WatchQueryOptions<TParsed>(
+ document: document,
+ operationName: operationName,
+ variables: variables,
+ fetchPolicy: fetchPolicy,
+ errorPolicy: errorPolicy,
+ cacheRereadPolicy: cacheRereadPolicy,
+ fetchResults: false,
+ context: context,
+ parserFn: parserFn,
+ );
}
/// Handles execution of mutation `update`, `onCompleted`, and `onError` callbacks

diff:
@@ -330,7 +330,7 @@ class ObservableQuery<TParsed> {
scheduler!.stopPollingQuery(queryId);
}
- options.pollInterval = pollInterval;
+ options = options.copyWithPollInterval(pollInterval);
lifecycle = QueryLifecycle.polling;
scheduler!.startPollingQuery(options, queryId);
}
@@ -338,13 +338,18 @@ class ObservableQuery<TParsed> {
void stopPolling() {
if (isCurrentlyPolling) {
scheduler!.stopPollingQuery(queryId);
- options.pollInterval = null;
+ options = options.copyWithPollInterval(null);
lifecycle = QueryLifecycle.pollingStopped;
}
}
- set variables(Map<String, dynamic> variables) =>
- options.variables = variables;
+ set variables(Map<String, dynamic> variables) {
+ options = options.copyWithVariables(variables);
+ }
+
+ set optimisticResult(Object? optimisticResult) {
+ options = options.copyWithOptimisticResult(optimisticResult);
+ }
/// [onData] callbacks have het to be run
///

diff:
@@ -343,12 +343,10 @@ class QueryManager {
/// Refetch the [ObservableQuery] referenced by [queryId],
/// overriding any present non-network-only [FetchPolicy].
Future<QueryResult<TParsed>?> refetchQuery<TParsed>(String queryId) {
- final WatchQueryOptions<TParsed> options =
- queries[queryId]!.options.copy() as WatchQueryOptions<TParsed>;
+ WatchQueryOptions<TParsed> options =
+ queries[queryId]!.options as WatchQueryOptions<TParsed>;
if (!willAlwaysExecuteOnNetwork(options.fetchPolicy)) {
- options.policies = options.policies.copyWith(
- fetch: FetchPolicy.networkOnly,
- );
+ options = options.copyWithFetchPolicy(FetchPolicy.networkOnly);
}
// create a new request to execute

diff:
-// ignore_for_file: deprecated_member_use_from_same_package
import 'package:graphql/src/core/_base_options.dart';
import 'package:graphql/src/core/result_parser.dart';
import 'package:graphql/src/utilities/helpers.dart';
@@ -6,8 +5,10 @@ import 'package:graphql/src/utilities/helpers.dart';
import 'package:gql/ast.dart';
import 'package:graphql/client.dart';
+import 'package:meta/meta.dart';
/// Query options.
+@immutable
class QueryOptions<TParsed> extends BaseOptions<TParsed> {
QueryOptions({
required DocumentNode document,
@@ -33,10 +34,13 @@ class QueryOptions<TParsed> extends BaseOptions<TParsed> {
);
/// The time interval on which this query should be re-fetched from the server.
- Duration? pollInterval;
+ final Duration? pollInterval;
@override
- List<Object?> get properties => [...super.properties, pollInterval];
+ List<Object?> get properties => [
+ ...super.properties,
+ pollInterval,
+ ];
WatchQueryOptions<TParsed> asWatchQueryOptions({bool fetchResults = true}) =>
WatchQueryOptions(
@@ -52,8 +56,22 @@ class QueryOptions<TParsed> extends BaseOptions<TParsed> {
optimisticResult: optimisticResult,
parserFn: this.parserFn,
);
+
+ QueryOptions<TParsed> copyWithPolicies(Policies policies) => QueryOptions(
+ document: document,
+ operationName: operationName,
+ variables: variables,
+ fetchPolicy: policies.fetch,
+ errorPolicy: policies.error,
+ cacheRereadPolicy: policies.cacheReread,
+ optimisticResult: optimisticResult,
+ pollInterval: pollInterval,
+ context: context,
+ parserFn: parserFn,
+ );
}
+@immutable
class SubscriptionOptions<TParsed> extends BaseOptions<TParsed> {
SubscriptionOptions({
required DocumentNode document,
@@ -76,11 +94,21 @@ class SubscriptionOptions<TParsed> extends BaseOptions<TParsed> {
optimisticResult: optimisticResult,
parserFn: parserFn,
);
-
- /// An optimistic first result to eagerly add to the subscription stream
- Object? optimisticResult;
+ SubscriptionOptions<TParsed> copyWithPolicies(Policies policies) =>
+ SubscriptionOptions(
+ document: document,
+ operationName: operationName,
+ variables: variables,
+ fetchPolicy: policies.fetch,
+ errorPolicy: policies.error,
+ cacheRereadPolicy: policies.cacheReread,
+ optimisticResult: optimisticResult,
+ context: context,
+ parserFn: parserFn,
+ );
}
+@immutable
class WatchQueryOptions<TParsed> extends QueryOptions<TParsed> {
WatchQueryOptions({
required DocumentNode document,
@@ -111,21 +139,99 @@ class WatchQueryOptions<TParsed> extends QueryOptions<TParsed> {
);
/// Whether or not to fetch results
- bool fetchResults;
+ final bool fetchResults;
/// Whether to [fetchResults] immediately on instantiation.
/// Defaults to [fetchResults].
- bool eagerlyFetchResults;
+ final bool eagerlyFetchResults;
/// carry forward previous data in the result of errors and no data.
/// defaults to `true`.
- bool carryForwardDataOnException;
+ final bool carryForwardDataOnException;
@override
- List<Object?> get properties =>
- [...super.properties, fetchResults, eagerlyFetchResults];
+ List<Object?> get properties => [
+ ...super.properties,
+ fetchResults,
+ eagerlyFetchResults,
+ carryForwardDataOnException,
+ ];
+
+ WatchQueryOptions<TParsed> copyWithFetchPolicy(
+ FetchPolicy? fetchPolicy,
+ ) =>
+ WatchQueryOptions<TParsed>(
+ document: document,
+ operationName: operationName,
+ variables: variables,
+ fetchPolicy: fetchPolicy,
+ errorPolicy: errorPolicy,
+ cacheRereadPolicy: cacheRereadPolicy,
+ optimisticResult: optimisticResult,
+ pollInterval: pollInterval,
+ fetchResults: fetchResults,
+ eagerlyFetchResults: eagerlyFetchResults,
+ carryForwardDataOnException: carryForwardDataOnException,
+ context: context,
+ parserFn: parserFn,
+ );
+ WatchQueryOptions<TParsed> copyWithPolicies(
+ Policies policies,
+ ) =>
+ WatchQueryOptions<TParsed>(
+ document: document,
+ operationName: operationName,
+ variables: variables,
+ fetchPolicy: policies.fetch,
+ errorPolicy: policies.error,
+ cacheRereadPolicy: policies.cacheReread,
+ optimisticResult: optimisticResult,
+ pollInterval: pollInterval,
+ fetchResults: fetchResults,
+ eagerlyFetchResults: eagerlyFetchResults,
+ carryForwardDataOnException: carryForwardDataOnException,
+ context: context,
+ parserFn: parserFn,
+ );
+
+ WatchQueryOptions<TParsed> copyWithPollInterval(Duration? pollInterval) =>
+ WatchQueryOptions<TParsed>(
+ document: document,
+ operationName: operationName,
+ variables: variables,
+ fetchPolicy: fetchPolicy,
+ errorPolicy: errorPolicy,
+ cacheRereadPolicy: cacheRereadPolicy,
+ optimisticResult: optimisticResult,
+ pollInterval: pollInterval,
+ fetchResults: fetchResults,
+ eagerlyFetchResults: eagerlyFetchResults,
+ carryForwardDataOnException: carryForwardDataOnException,
+ context: context,
+ parserFn: parserFn,
+ );
+
+ WatchQueryOptions<TParsed> copyWithVariables(
+ Map<String, dynamic> variables) =>
+ WatchQueryOptions<TParsed>(
+ document: document,
+ operationName: operationName,
+ variables: variables,
+ fetchPolicy: fetchPolicy,
+ errorPolicy: errorPolicy,
+ cacheRereadPolicy: cacheRereadPolicy,
+ optimisticResult: optimisticResult,
+ pollInterval: pollInterval,
+ fetchResults: fetchResults,
+ eagerlyFetchResults: eagerlyFetchResults,
+ carryForwardDataOnException: carryForwardDataOnException,
+ context: context,
+ parserFn: parserFn,
+ );
- WatchQueryOptions<TParsed> copy() => WatchQueryOptions<TParsed>(
+ WatchQueryOptions<TParsed> copyWithOptimisticResult(
+ Object? optimisticResult) =>
+ WatchQueryOptions<TParsed>(
document: document,
operationName: operationName,
variables: variables,
@@ -148,6 +254,7 @@ class WatchQueryOptions<TParsed> extends QueryOptions<TParsed> {
/// it is easy to make mistakes in writing [updateQuery].
///
/// To mitigate this, [FetchMoreOptions.partial] has been provided.
+@immutable
class FetchMoreOptions {
FetchMoreOptions({
this.document,
@@ -171,13 +278,13 @@ class FetchMoreOptions {
updateQuery: partialUpdater(updateQuery),
);
- DocumentNode? document;
+ final DocumentNode? document;
- Map<String, dynamic> variables;
+ final Map<String, dynamic> variables;
/// Strategy for merging the fetchMore result data
/// with the result data already in the cache
- UpdateQuery updateQuery;
+ final UpdateQuery updateQuery;
/// Wrap an [UpdateQuery] in a [deeplyMergeLeft] of the `previousResultData`.
static UpdateQuery partialUpdater(UpdateQuery update) =>

diff:
@@ -89,9 +89,8 @@ class GraphQLClient implements GraphQLDataProxy {
/// {@end-tool}
ObservableQuery<TParsed> watchQuery<TParsed>(
WatchQueryOptions<TParsed> options) {
- options.policies =
- defaultPolicies.watchQuery.withOverrides(options.policies);
- return queryManager.watchQuery(options);
+ final policies = defaultPolicies.watchQuery.withOverrides(options.policies);
+ return queryManager.watchQuery(options.copyWithPolicies(policies));
}
/// [watchMutation] is the same as [watchQuery], but with a different [defaultPolicies] that are more appropriate for mutations.
@@ -101,9 +100,9 @@ class GraphQLClient implements GraphQLDataProxy {
/// For more details, see https://github.com/zino-app/graphql-flutter/issues/774
ObservableQuery<TParsed> watchMutation<TParsed>(
WatchQueryOptions<TParsed> options) {
- options.policies =
+ final policies =
defaultPolicies.watchMutation.withOverrides(options.policies);
- return queryManager.watchQuery(options);
+ return queryManager.watchQuery(options.copyWithPolicies(policies));
}
/// This resolves a single query according to the [QueryOptions] specified and
@@ -149,16 +148,16 @@ class GraphQLClient implements GraphQLDataProxy {
Future<QueryResult<TParsed>> query<TParsed>(
QueryOptions<TParsed> options,
) async {
- options.policies = defaultPolicies.query.withOverrides(options.policies);
- return await queryManager.query(options);
+ final policies = defaultPolicies.query.withOverrides(options.policies);
+ return await queryManager.query(options.copyWithPolicies(policies));
}
/// This resolves a single mutation according to the [MutationOptions] specified and
/// returns a [Future] which resolves with the [QueryResult] or throws an [Exception].
Future<QueryResult<TParsed>> mutate<TParsed>(
MutationOptions<TParsed> options) async {
- options.policies = defaultPolicies.mutate.withOverrides(options.policies);
- return await queryManager.mutate(options);
+ final policies = defaultPolicies.mutate.withOverrides(options.policies);
+ return await queryManager.mutate(options.copyWithPolicies(policies));
}
/// This subscribes to a GraphQL subscription according to the options specified and returns a
@@ -199,10 +198,10 @@ class GraphQLClient implements GraphQLDataProxy {
/// {@end-tool}
Stream<QueryResult<TParsed>> subscribe<TParsed>(
SubscriptionOptions<TParsed> options) {
- options.policies = defaultPolicies.subscribe.withOverrides(
+ final policies = defaultPolicies.subscribe.withOverrides(
options.policies,
);
- return queryManager.subscribe(options);
+ return queryManager.subscribe(options.copyWithPolicies(policies));
}
/// Fetch more results and then merge them with the given [previousResult]

diff:
@@ -5,6 +5,29 @@ import 'package:test/test.dart';
void main() {
group('query options', () {
+ group('equality', () {
+ test('compares content', () {
+ final fn = (_d) => null;
+ final t1 = QueryOptions(
+ variables: {'foo': 'bar'},
+ document: parseString('query { bar }'),
+ parserFn: fn,
+ );
+ final t2 = QueryOptions(
+ variables: {'foo': 'bar'},
+ document: parseString('query { bar }'),
+ parserFn: fn,
+ );
+ final t3 = QueryOptions(
+ document: parseString('query { barz }'),
+ parserFn: fn,
+ );
+ expect(t1, equals(t2));
+ expect(t1.hashCode, equals(t2.hashCode));
+ expect(t1, isNot(equals(t3)));
+ expect(t1.hashCode, isNot(equals(t3.hashCode)));
+ });
+ });
group('type getters', () {
test('on QueryOptions', () {
final options = QueryOptions(

original_message: feat: Update options