message: refactor unit test and get test coverage

change_type: ADD

diff:
+container:
+ image: cirrusci/flutter:latest
+
+test_task:
+ # pub_cache:
+ # folder: $HOME/.pub-cache
+ # fingerprint_script: cat pubspec.lock
+ # populate_script: pub get
+ pub_script: flutter packages get # later when we seperate graphql_client and graphql_flutter, we can change this
+ # format_script: dartfmt **/*.dart -n --set-exit-if-changed
+ # analyze_script: dartanalyzer --fatal-infos .
+ # publishable_script: pub publish --dry-run
+ environment:
+ CODECOV_TOKEN: ENCRYPTED[fa57a1dda8bca6ad45ef10043f632b68b7824b60c634415f741a847788c1a44ae9374155e3af3346fb9b6d72bac0e1d3]
+ test_script:
+ # - pub global activate coverage
+ # - dart --enable-vm-service=8111 --pause-isolates-on-exit test/* &
+ # - pub global run coverage:collect_coverage --port=8111 --out=coverage.json --wait-paused --resume-isolates
+ # - pub global run coverage:format_coverage --lcov --in=coverage.json --out=lcov.info --packages=.packages --report-on=lib
+ - flutter test --coverage
+ - bash <(curl -s https://codecov.io/bash)

old_path: .gitignore

new_path: .gitignore

diff:
@@ -20,3 +20,7 @@ ios/Runner/GeneratedPluginRegistrant.*
example/lib/local.dart
+
+# Coverage
+coverage.json
+coverage/*

diff:
+import 'package:rxdart/subjects.dart';
+import 'package:graphql_flutter/graphql_flutter.dart';
+
+import './mutations/mutations.dart' as mutations;
+import './queries/readRepositories.dart' as queries;
+
+
+const String YOUR_PERSONAL_ACCESS_TOKEN =
+ '';
+
+class Repo {
+ const Repo({this.id, this.name, this.viewerHasStarred});
+ final String id;
+ final String name;
+ final bool viewerHasStarred;
+}
+
+class Bloc {
+ Bloc() {
+ _queryRepo();
+ _updateNumberOfRepo.listen((int n) async => _queryRepo(nRepositories: n));
+ _toggleStarSubject.listen((Repo t) async {
+ _toggleStarLoadingSubject.add(true);
+ // @todo handle error
+ final r = await _mutateToggleStar(t);
+
+ _repoSubject.add(_repoSubject.value.map((Repo e) {
+ if (e.id != t.id) {
+ return e;
+ }
+ return Repo(
+ id: t.id, name: t.name, viewerHasStarred: !t.viewerHasStarred);
+ }).toList());
+ _toggleStarLoadingSubject.add(false);
+ });
+ }
+
+ final BehaviorSubject<List<Repo>> _repoSubject =
+ BehaviorSubject<List<Repo>>();
+ Stream<List<Repo>> get repoStream => _repoSubject.stream;
+
+ final ReplaySubject<Repo> _toggleStarSubject = ReplaySubject<Repo>();
+ Sink<Repo> get toggleStarSink => _toggleStarSubject;
+
+ final BehaviorSubject<bool> _toggleStarLoadingSubject =
+ BehaviorSubject<bool>();
+ Stream<bool> get toggleStarLoadingStream => _toggleStarLoadingSubject.stream;
+
+ final BehaviorSubject<int> _updateNumberOfRepo = BehaviorSubject<int>();
+
+ Sink<int> get updateNumberOfRepoSink => _updateNumberOfRepo;
+
+ static final HttpLink _httpLink = HttpLink(
+ uri: 'https://api.github.com/graphql',
+ );
+
+ static final AuthLink _authLink = AuthLink(
+ getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
+ );
+
+ static final Link _link = _authLink.concat(_httpLink);
+
+ static final GraphQLClient _client = GraphQLClient(
+ cache: NormalizedInMemoryCache(
+ dataIdFromObject: typenameDataIdFromObject,
+ ),
+ link: _link,
+ );
+
+ Future<void> _mutateToggleStar(Repo repo) async {
+ final MutationOptions _options = MutationOptions(
+ document:
+ repo.viewerHasStarred ? mutations.removeStar : mutations.addStar,
+ variables: <String, String>{
+ 'starrableId': repo.id,
+ },
+// fetchPolicy: widget.options.fetchPolicy,
+// errorPolicy: widget.options.errorPolicy,
+ );
+
+ final QueryResult result = await _client.mutate(_options);
+ return result;
+ }
+
+ Future<void> _queryRepo({int nRepositories = 50}) async {
+ // null is loading
+ _repoSubject.add(null);
+// FetchPolicy fetchPolicy = widget.options.fetchPolicy;
+//
+// if (fetchPolicy == FetchPolicy.cacheFirst) {
+// fetchPolicy = FetchPolicy.cacheAndNetwork;
+// }
+ final WatchQueryOptions _options = WatchQueryOptions(
+ document: queries.readRepositories,
+ variables: <String, dynamic>{
+ 'nRepositories': nRepositories,
+ },
+// fetchPolicy: fetchPolicy,
+// errorPolicy: widget.options.errorPolicy,
+ pollInterval: 4,
+ fetchResults: true,
+// context: widget.options.context,
+ );
+
+ final QueryResult result = await _client.query(_options);
+
+ if (result.hasErrors) {
+ _repoSubject.addError(result.errors);
+ return;
+ }
+
+ // result.data can be either a [List<dynamic>] or a [Map<String, dynamic>]
+ final List<dynamic> repositories =
+ result.data['viewer']['repositories']['nodes'] as List<dynamic>;
+
+ _repoSubject.add(repositories
+ .map((dynamic e) => Repo(
+ id: e['id'] as String,
+ name: e['name'] as String,
+ viewerHasStarred: e['viewerHasStarred'] as bool,
+ ))
+ .toList());
+ }
+}
+
+/**
+ *
+ *
+ key: Key(starred.toString()),
+ options: MutationOptions(
+ document: starred ? mutations.removeStar : mutations.addStar,
+
+ builder: (RunMutation toggleStar, QueryResult result) {
+
+
+ toggleStar(<String, dynamic>{
+ 'starrableId': widget.repository['id'],
+ });
+ ),
+
+
+
+ update: (Cache cache, QueryResult result) {
+ if (result.hasErrors) {
+ print(result.errors);
+ } else {
+ final Map<String, Object> updated =
+ Map<String, Object>.from(widget.repository)
+ ..addAll(extractRepositoryData(result.data as Map<String, Object>));
+
+ cache.write(typenameDataIdFromObject(updated), updated);
+ }
+ },
+ onCompleted: (QueryResult result) {
+ showDialog<AlertDialog>(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text(
+ extractRepositoryData(result.data as Map<String, Object>)['viewerHasStarred'] as bool
+ ? 'Thanks for your star!'
+ : 'Sorry you changed your mind!',
+ ),
+ actions: <Widget>[
+ SimpleDialogOption(
+ child: const Text('Dismiss'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ )
+ ],
+ );
+ },
+ );
+ setState(() {
+ loading = false;
+ });
+ },
+ */

diff:
import 'package:flutter/material.dart';
-import 'package:graphql_flutter/graphql_flutter.dart';
-import './mutations/mutations.dart' as mutations;
-import './queries/readRepositories.dart' as queries;
-
-const String YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN>';
+import 'bloc.dart' show Bloc, Repo;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final HttpLink httpLink = HttpLink(
- uri: 'https://api.github.com/graphql',
- );
-
- final AuthLink authLink = AuthLink(
- getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
- );
-
- final Link link = authLink.concat(httpLink);
-
- final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
- GraphQLClient(
- cache: NormalizedInMemoryCache(
- dataIdFromObject: typenameDataIdFromObject,
- ),
- link: link,
- ),
- );
-
- return GraphQLProvider(
- client: client,
- child: CacheProvider(
- child: MaterialApp(
+ return MaterialApp(
title: 'GraphQL Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
- home: const MyHomePage(title: 'GraphQL Flutter Home Page'),
- ),
- ),
+ home: MyHomePage(title: 'GraphQL Flutter Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
- const MyHomePage({
+ MyHomePage({
Key key,
this.title,
- }) : super(key: key);
+ }) : bloc = Bloc(),
+ super(key: key);
final String title;
+ final Bloc bloc;
@override
- _MyHomePageState createState() => _MyHomePageState();
+ _MyHomePageState createState() => _MyHomePageState(bloc);
}
class _MyHomePageState extends State<MyHomePage> {
- int nRepositories = 50;
-
- void changeQuery(String number) {
- setState(() {
- nRepositories = int.parse(number) ?? 50;
- });
- }
+ _MyHomePageState(this.bloc);
+ final Bloc bloc;
@override
Widget build(BuildContext context) {
@@ -83,36 +52,31 @@ class _MyHomePageState extends State<MyHomePage> {
labelText: 'Number of repositories (default 50)',
),
keyboardType: TextInputType.number,
- onSubmitted: changeQuery,
+ onChanged: (String n) =>
+ bloc.updateNumberOfRepoSink.add(int.parse(n)),
),
- Query(
- options: QueryOptions(
- document: queries.readRepositories,
- variables: <String, dynamic>{
- 'nRepositories': nRepositories,
- },
- pollInterval: 4,
- ),
- builder: (QueryResult result) {
- if (result.loading) {
+ StreamBuilder<List<Repo>>(
+ stream: bloc.repoStream,
+ builder: (BuildContext context,
+ AsyncSnapshot<List<Repo>> snapshot) {
+ if (snapshot.hasError) {
+ return Text('\nErrors: \n ' +
+ (snapshot.error as List<dynamic>).join(',\n '));
+ }
+ if (snapshot.data == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
- if (result.hasErrors) {
- return Text('\nErrors: \n ' + result.errors.join(',\n '));
- }
-
- // result.data can be either a [List<dynamic>] or a [Map<String, dynamic>]
- final List<dynamic> repositories =
- result.data['viewer']['repositories']['nodes'] as List<dynamic>;
+ final List<Repo> repositories = snapshot.data;
return Expanded(
child: ListView.builder(
itemCount: repositories.length,
itemBuilder: (BuildContext context, int index) =>
- StarrableRepository(repository: repositories[index] as Map<String, Object>),
+ StarrableRepository(
+ repository: repositories[index], bloc: bloc),
),
);
},
@@ -124,22 +88,15 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
-class StarrableRepository extends StatefulWidget {
+class StarrableRepository extends StatelessWidget {
const StarrableRepository({
Key key,
@required this.repository,
+ @required this.bloc,
}) : super(key: key);
- final Map<String, Object> repository;
-
- @override
- StarrableRepositoryState createState() {
- return StarrableRepositoryState();
- }
-}
-
-class StarrableRepositoryState extends State<StarrableRepository> {
- bool loading = false;
+ final Bloc bloc;
+ final Repo repository;
Map<String, Object> extractRepositoryData(Map<String, Object> data) {
final Map<String, Object> action = data['action'] as Map<String, Object>;
@@ -151,74 +108,28 @@ class StarrableRepositoryState extends State<StarrableRepository> {
return action['starrable'] as Map<String, Object>;
}
- bool get viewerHasStarred => widget.repository['viewerHasStarred'] as bool;
+ bool get viewerHasStarred => repository.viewerHasStarred;
@override
Widget build(BuildContext context) {
- final bool starred = loading ? !viewerHasStarred : viewerHasStarred;
-
- return Mutation(
- key: Key(starred.toString()),
- options: MutationOptions(
- document: starred ? mutations.removeStar : mutations.addStar,
- ),
- builder: (RunMutation toggleStar, QueryResult result) {
+ return StreamBuilder<bool>(
+ stream: bloc.toggleStarLoadingStream,
+ initialData: false,
+ builder: (BuildContext context, AsyncSnapshot<bool> result) {
+ final bool loading = result.data;
return ListTile(
- leading: starred
+ leading: viewerHasStarred
? const Icon(
Icons.star,
color: Colors.amber,
)
: const Icon(Icons.star_border),
trailing: loading ? const CircularProgressIndicator() : null,
- title: Text(widget.repository['name'] as String),
+ title: Text(repository.name),
onTap: () {
- // optimistic ui updates are not implemented yet,
- // so we track loading manually
- setState(() {
- loading = true;
- });
- toggleStar(<String, dynamic>{
- 'starrableId': widget.repository['id'],
- });
- },
- );
- },
- update: (Cache cache, QueryResult result) {
- if (result.hasErrors) {
- print(result.errors);
- } else {
- final Map<String, Object> updated =
- Map<String, Object>.from(widget.repository)
- ..addAll(extractRepositoryData(result.data as Map<String, Object>));
-
- cache.write(typenameDataIdFromObject(updated), updated);
- }
- },
- onCompleted: (QueryResult result) {
- showDialog<AlertDialog>(
- context: context,
- builder: (BuildContext context) {
- return AlertDialog(
- title: Text(
- extractRepositoryData(result.data as Map<String, Object>)['viewerHasStarred'] as bool
- ? 'Thanks for your star!'
- : 'Sorry you changed your mind!',
- ),
- actions: <Widget>[
- SimpleDialogOption(
- child: const Text('Dismiss'),
- onPressed: () {
- Navigator.of(context).pop();
- },
- )
- ],
- );
+ bloc.toggleStarSink.add(repository);
},
);
- setState(() {
- loading = false;
- });
},
);
}

diff:
@@ -5,6 +5,7 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
+ rxdart: 0.21.0
graphql_flutter:
path: ..

diff:
import 'dart:async';
import 'dart:convert';
+import 'dart:io';
import 'package:meta/meta.dart';
import 'package:http/http.dart';
@@ -15,7 +16,9 @@ class HttpLink extends Link {
HttpLink({
@required String uri,
bool includeExtensions,
- Client fetch,
+
+ /// pass on customized httpClient, especially handy for mocking and testing
+ Client httpClient,
Map<String, String> headers,
Map<String, dynamic> credentials,
Map<String, dynamic> fetchOptions,
@@ -24,7 +27,7 @@ class HttpLink extends Link {
Operation operation, [
NextLink forward,
]) {
- final Client fetcher = fetch ?? Client();
+ final Client fetcher = httpClient ?? Client();
final HttpConfig linkConfig = HttpConfig(
http: HttpQueryOptions(
@@ -59,26 +62,26 @@ class HttpLink extends Link {
);
final Map<String, dynamic> options = httpOptionsAndBody.options;
- final Map<String, String> httpHeaders = options['headers'] as Map<String, String>;
+ final Map<String, String> httpHeaders =
+ options['headers'] as Map<String, String>;
StreamController<FetchResult> controller;
Future<void> onListen() async {
- Response response;
+ StreamedResponse response;
try {
- // TODO: support multiple http methods
- response = await fetcher.post(
- uri,
- headers: httpHeaders,
- body: httpOptionsAndBody.body,
- );
- operation.setContext(<String, Response>{
+ Request r = Request('post', Uri.parse(uri))
+ ..headers.addAll(httpHeaders)
+ ;
+ r.body = httpOptionsAndBody.body as String;
+ response = await fetcher.send(r);
+
+ operation.setContext(<String, StreamedResponse>{
'response': response,
});
-
- final FetchResult parsedResponse = _parseResponse(response);
+ final FetchResult parsedResponse = await _parseResponse(response);
controller.add(parsedResponse);
} catch (error) {
@@ -184,25 +187,31 @@ HttpOptionsAndBody _selectHttpOptionsAndBody(
return HttpOptionsAndBody(
options: options,
- body: json.encode(body),
+ body: encodeBody(body),
);
}
-FetchResult _parseResponse(Response response) {
+dynamic encodeBody(dynamic body) {
+ final encodedBody = json.encode(body, toEncodable: (dynamic object) {
+ if(object is File){
+ return null;
+ }
+ return object.toJson();
+ });
+ return encodedBody;
+}
+
+Future<FetchResult> _parseResponse(StreamedResponse response) async {
final int statusCode = response.statusCode;
final String reasonPhrase = response.reasonPhrase;
- /* TODO this discards a lot of useful info in development */
- if (statusCode < 200 || statusCode >= 400) {
- throw ClientException(
- 'Network Error: $statusCode $reasonPhrase',
- );
- }
-
+ try {
final Encoding encoding = _determineEncodingFromResponse(response);
- final String decodedBody = encoding.decode(response.bodyBytes);
+ // @todo limit bodyBytes
+ final String decodedBody = encoding.decode(await response.stream.toBytes());
- final Map<String, dynamic> jsonResponse = json.decode(decodedBody) as Map<String, dynamic>;
+ final Map<String, dynamic> jsonResponse =
+ json.decode(decodedBody) as Map<String, dynamic>;
final FetchResult fetchResult = FetchResult();
if (jsonResponse['errors'] != null) {
@@ -214,6 +223,15 @@ FetchResult _parseResponse(Response response) {
}
return fetchResult;
+ } catch (e) {
+ // @todo get body as well;
+ // final String decodedBody = encoding.decode(response.bodyBytes);
+ if (statusCode < 200 || statusCode >= 400) {
+ throw ClientException(
+ 'Network Error: $statusCode $reasonPhrase',
+ );
+ }
+ }
}
/// Returns the charset encoding for the given response.
@@ -221,7 +239,7 @@ FetchResult _parseResponse(Response response) {
/// The default fallback encoding is set to UTF-8 according to the IETF RFC4627 standard
/// which specifies the application/json media type:
/// "JSON text SHALL be encoded in Unicode. The default encoding is UTF-8."
-Encoding _determineEncodingFromResponse(Response response,
+Encoding _determineEncodingFromResponse(BaseResponse response,
[Encoding fallback = utf8]) {
final String contentType = response.headers['content-type'];

diff:
@@ -18,9 +18,10 @@ dependencies:
dev_dependencies:
perfectionist: ^2.0.0
+ mockito: ^4.0.0
flutter_test:
sdk: flutter
test: ^1.3.0
environment:
- sdk: ">=2.2.0 <3.0.0"
+ sdk: ">=2.0.0 <3.0.0"

diff:
-// import 'package:test/test.dart';
+import 'dart:convert';
+import 'dart:io' show File, Platform;
-// import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:test/test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:http/http.dart' as http;
+import 'package:graphql_flutter/graphql_flutter.dart';
+
+class MockHttpClient extends Mock implements http.Client {}
void main() {
- // TODO: write tests
+ const String readRepositories = r'''
+ query ReadRepositories($nRepositories: Int!) {
+ viewer {
+ repositories(last: $nRepositories) {
+ nodes {
+ __typename
+ id
+ name
+ viewerHasStarred
+ }
+ }
+ }
+ }
+''';
+
+ const String addStar = r'''
+ mutation AddStar($starrableId: ID!) {
+ action: addStar(input: {starrableId: $starrableId}) {
+ starrable {
+ viewerHasStarred
+ }
+ }
+ }
+''';
+
+ HttpLink httpLink;
+ AuthLink authLink;
+ Link link;
+ GraphQLClient graphQLClientClient;
+ MockHttpClient mockHttpClient;
+
+ setUp(() {
+ mockHttpClient = MockHttpClient();
+
+ httpLink = HttpLink(
+ uri: 'https://api.github.com/graphql', httpClient: mockHttpClient);
+
+ authLink = AuthLink(
+ getToken: () async => 'Bearer my-special-bearer-token',
+ );
+
+ link = authLink.concat(httpLink);
+
+ graphQLClientClient = GraphQLClient(
+ cache: NormalizedInMemoryCache(
+ dataIdFromObject: typenameDataIdFromObject,
+ ),
+ link: link,
+ );
+ });
+ group('query', () {
+ test('successful query', () async {
+ final WatchQueryOptions _options = WatchQueryOptions(
+ document: readRepositories,
+ variables: <String, dynamic>{
+ 'nRepositories': 42,
+ },
+ );
+ when(
+ mockHttpClient.send(any),
+ ).thenAnswer((Invocation a) async {
+ const String body = '''
+{
+ "data": {
+ "viewer": {
+ "repositories": {
+ "nodes": [
+ {
+ "__typename": "Repository",
+ "id": "MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==",
+ "name": "pq",
+ "viewerHasStarred": false
+ },
+ {
+ "__typename": "Repository",
+ "id": "MDEwOlJlcG9zaXRvcnkzMjkyNDQ0Mw==",
+ "name": "go-evercookie",
+ "viewerHasStarred": false
+ },
+ {
+ "__typename": "Repository",
+ "id": "MDEwOlJlcG9zaXRvcnkzNTA0NjgyNA==",
+ "name": "watchbot",
+ "viewerHasStarred": false
+ }
+ ]
+ }
+ }
+ }
+}
+ ''';
+
+ final List<int> bytes = utf8.encode(body);
+ final Stream<List<int>> stream =
+ Stream<List<int>>.fromIterable([bytes]);
+
+ final http.StreamedResponse r = http.StreamedResponse(stream, 200);
+
+ return r;
+ });
+ final QueryResult r = await graphQLClientClient.query(_options);
+
+ final http.Request capt = verify(mockHttpClient.send(captureAny))
+ .captured
+ .first as http.Request;
+ expect(capt.method, 'post');
+ expect(capt.url.toString(), 'https://api.github.com/graphql');
+ expect(
+ capt.headers,
+ <String, String>{
+ 'accept': '*/*',
+ 'content-type': 'application/json; charset=utf-8',
+ 'Authorization': 'Bearer my-special-bearer-token',
+ },
+ );
+ expect(await capt.finalize().bytesToString(), r'{"operationName":"ReadRepositories","variables":{"nRepositories":42},"query":" query ReadRepositories($nRepositories: Int!) {\n viewer {\n repositories(last: $nRepositories) {\n nodes {\n __typename\n id\n name\n viewerHasStarred\n }\n }\n }\n }\n"}');
+
+ expect(r.errors, isNull);
+ expect(r.data, isNotNull);
+ final List<Map<String, dynamic>> nodes =
+ (r.data['viewer']['repositories']['nodes'] as List<dynamic>)
+ .cast<Map<String, dynamic>>();
+ expect(nodes, hasLength(3));
+ expect(nodes[0]['id'], 'MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==');
+ expect(nodes[1]['name'], 'go-evercookie');
+ expect(nodes[2]['viewerHasStarred'], false);
+ return;
+ });
+// test('failed query because of network', {});
+// test('failed query because of because of error response', {});
+// test('failed query because of because of invalid response', {});
+ });
+ group('mutation', () {
+ test('successful mutation', () async {
+ final MutationOptions _options = MutationOptions(
+ document: addStar,
+ variables: <String, dynamic>{
+ 'nRepositories': 38,
+ },
+ );
+ when(
+ mockHttpClient.send(any)
+ ).thenAnswer((Invocation a) async {
+ const String body =
+ '{"data":{"action":{"starrable":{"viewerHasStarred":true}}}}';
+
+ final List<int> bytes = utf8.encode(body);
+ final Stream<List<int>> stream =
+ Stream<List<int>>.fromIterable(<List<int>>[bytes]);
+
+ final http.StreamedResponse r = http.StreamedResponse(stream, 200);
+ return r;
+ });
+
+ final QueryResult r = await graphQLClientClient.mutate(_options);
+
+ final http.Request capt = verify(mockHttpClient.send(captureAny))
+ .captured
+ .first as http.Request;
+ expect(capt.method, 'post');
+ expect(capt.url.toString(), 'https://api.github.com/graphql');
+ expect(
+ capt.headers,
+ <String, String>{
+ 'accept': '*/*',
+ 'content-type': 'application/json; charset=utf-8',
+ 'Authorization': 'Bearer my-special-bearer-token',
+ },
+ );
+ expect(await capt.finalize().bytesToString(), r'{"operationName":"AddStar","variables":{"nRepositories":38},"query":" mutation AddStar($starrableId: ID!) {\n action: addStar(input: {starrableId: $starrableId}) {\n starrable {\n viewerHasStarred\n }\n }\n }\n"}');
+
+ expect(r.errors, isNull);
+ expect(r.data, isNotNull);
+ final bool viewerHasStarred =
+ r.data['action']['starrable']['viewerHasStarred'] as bool;
+ expect(viewerHasStarred, true);
+ });
+ });
+ group('upload', () {
+ const String uploadMutation = r'''
+ mutation($files: [Upload!]!) {
+ multipleUpload(files: $files) {
+ id
+ filename
+ mimetype
+ path
+ }
+ }
+
+ ''';
+// test('upload success', () async {
+// final List<File> files = <String>['sample_upload.bin', 'sample_upload.img']
+// .map((String fileName) => join(
+// dirname(Platform.script.path),
+// 'test',
+// ))
+// .map((String filePath) => File(filePath))
+// .toList();
+//
+// final MutationOptions _options = MutationOptions(
+// document: uploadMutation,
+// variables: <String, dynamic>{
+// 'files': files,
+// },
+// );
+//
+//
+// final QueryResult r = await graphQLClientClient.mutate(_options);
+// expect(r.errors, isNull);
+// expect(r.data, isNotNull);
+// final bool viewerHasStarred =
+// r.data['action']['starrable']['viewerHasStarred'] as bool;
+// expect(viewerHasStarred, true);
+// });
+ });
}


message: build status and unit test coverage

change_type: MODIFY

diff:
# GraphQL Flutter
-[![version][version-badge]][package]
-[![MIT License][license-badge]][license]
+[![Build Status][build-status-badge]][build-status-link]
+[![Coverage][coverage-badge]][coverage-link]
+[![version][version-badge]][package-link]
+[![MIT License][license-badge]][license-link]
[![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg?style=flat-square)](#contributors)
[![PRs Welcome][prs-badge]](http://makeapullrequest.com)
@@ -466,10 +468,14 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind are welcome!
+[build-status-badge]: https://api.cirrus-ci.com/github/truongsinh/graphql-flutter.svg
+[build-status-link]: https://cirrus-ci.com/github/truongsinh/dart-uuid/master
+[coverage-badge]: https://codecov.io/gh/truongsinh/graphql-flutter/branch/master/graph/badge.svg
+[coverage-link]: https://codecov.io/gh/truongsinh/graphql-flutter
[version-badge]: https://img.shields.io/pub/v/graphql_flutter.svg?style=flat-square
-[package]: https://pub.dartlang.org/packages/graphql_flutter/versions/1.0.0-alpha.3
+[package-link]: https://pub.dartlang.org/packages/graphql_flutter/versions/1.0.0-alpha.3
[license-badge]: https://img.shields.io/github/license/zino-app/graphql-flutter.svg?style=flat-square
-[license]: https://github.com/zino-app/graphql-flutter/blob/master/LICENSE
+[license-link]: https://github.com/zino-app/graphql-flutter/blob/master/LICENSE
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[prs]: http://makeapullrequest.com
[github-watch-badge]: https://img.shields.io/github/watchers/zino-app/graphql-flutter.svg?style=social

license: MIT License

repo: zino-hofmann/graphql-flutter

original_message: build status and unit test coverage


new_path: .gitmodules

diff:
+[submodule "example/server"]
+ path = example/server
+ url = https://github.com/jaydenseric/apollo-upload-examples.git

diff:
@@ -58,7 +58,7 @@ class Bloc {
getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
);
- static final Link _link = _authLink.concat(_httpLink);
+ static final Link _link = _authLink.concat(_httpLink as Link);
static final GraphQLClient _client = GraphQLClient(
cache: NormalizedInMemoryCache(

diff:
@@ -119,7 +119,10 @@ class QueryManager {
).first;
// save the data from fetchResult to the cache
- if (fetchResult.data != null &&
+ if (
+ // should never cache a mutation
+ !(options is MutationOptions) &&
+ fetchResult.data != null &&
options.fetchPolicy != FetchPolicy.noCache) {
cache.write(
operation.toKey(),

diff:
@@ -32,12 +32,12 @@ class HttpConfig {
Map<String, String> headers;
}
-class HttpOptionsAndBody {
- HttpOptionsAndBody({
- this.options,
+class HttpHeadersAndBody {
+ HttpHeadersAndBody({
+ this.headers,
this.body,
});
- final Map<String, dynamic> options;
- final String body;
+ final Map<String, String> headers;
+ final Map<String, dynamic> body;
}

diff:
@@ -5,6 +5,8 @@ import 'dart:io';
import 'package:meta/meta.dart';
import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';
+import 'package:path/path.dart';
+import 'package:mime/mime.dart';
import 'package:graphql_flutter/src/link/link.dart';
import 'package:graphql_flutter/src/link/operation.dart';
@@ -53,7 +55,7 @@ class HttpLink extends Link {
);
}
- final HttpOptionsAndBody httpOptionsAndBody =
+ final HttpHeadersAndBody httpHeadersAndBody =
_selectHttpOptionsAndBody(
operation,
fallbackHttpConfig,
@@ -61,9 +63,7 @@ class HttpLink extends Link {
contextConfig,
);
- final Map<String, dynamic> options = httpOptionsAndBody.options;
- final Map<String, String> httpHeaders =
- options['headers'] as Map<String, String>;
+ final Map<String, String> httpHeaders = httpHeadersAndBody.headers;
StreamController<FetchResult> controller;
@@ -71,17 +71,17 @@ class HttpLink extends Link {
StreamedResponse response;
try {
+ // httpOptionsAndBody.body as String
+ final BaseRequest request = await _prepareRequest(
+ uri, httpHeadersAndBody.body, httpHeaders);
- Request r = Request('post', Uri.parse(uri))
- ..headers.addAll(httpHeaders)
- ;
- r.body = httpOptionsAndBody.body as String;
- response = await fetcher.send(r);
+ response = await fetcher.send(request);
operation.setContext(<String, StreamedResponse>{
'response': response,
});
- final FetchResult parsedResponse = await _parseResponse(response);
+ final FetchResult parsedResponse =
+ await _parseResponse(response);
controller.add(parsedResponse);
} catch (error) {
@@ -98,7 +98,95 @@ class HttpLink extends Link {
);
}
-HttpOptionsAndBody _selectHttpOptionsAndBody(
+Map<String, File> _getFileMap(
+ dynamic body, {
+ Map<String, File> currentMap,
+ List<String> currentPath = const <String>[],
+}) {
+ currentMap ??= <String, File>{};
+ if (body is Map<String, dynamic>) {
+ final entries = body.entries;
+ for (MapEntry<String, dynamic> element in entries) {
+ currentMap.addAll(_getFileMap(
+ element.value,
+ currentMap: currentMap,
+ currentPath: List<String>.from(currentPath)..add(element.key),
+ ));
+ }
+ return currentMap;
+ }
+ if (body is List<dynamic>) {
+ for (int i = 0; i < body.length; i++) {
+ currentMap.addAll(_getFileMap(
+ body[i],
+ currentMap: currentMap,
+ currentPath: List<String>.from(currentPath)..add(i.toString()),
+ ));
+ }
+ return currentMap;
+ }
+ if (body is File) {
+ return currentMap..addAll(<String, File>{currentPath.join('.'): body});
+ }
+ // else should only be either String, num, null; NOTHING else
+ return currentMap;
+}
+
+Future<BaseRequest> _prepareRequest(
+ String url,
+ Map<String, dynamic> body,
+ Map<String, String> httpHeaders,
+) async {
+ final Map<String, File> fileMap = _getFileMap(body);
+ if (fileMap.isEmpty) {
+ final Request r = Request('post', Uri.parse(url));
+ r.headers.addAll(httpHeaders);
+ r.body = json.encode(body);
+ return r;
+ }
+
+ final MultipartRequest r = MultipartRequest('post', Uri.parse(url));
+ r.headers.addAll(httpHeaders);
+ r.fields['operations'] = json.encode(body, toEncodable: (dynamic object) {
+ if (object is File) {
+ return null;
+ }
+ return object.toJson();
+ });
+
+ // @todo fileMap.keys.toList() and fileMap.values.toList() same order????
+ final Map<String, List<String>> adasd =
+ {}; // fileMap.keys.toList().asMap().map((int index, String filePath) => MapEntry(index.toString(),[filePath]));
+ final List<MultipartFile> fileList = [];
+
+ final fEn = fileMap.entries.toList(growable: false);
+
+ for (int i = 0; i < fEn.length; i++) {
+ final MapEntry<String, File> entry = fEn[i];
+ final String indexString = i.toString();
+ adasd.addAll({
+ indexString: [entry.key]
+ });
+ final File f = entry.value;
+ final String fileName = basename(f.path);
+ fileList.add(MultipartFile(
+ indexString,
+ f.openRead(),
+ await f.length(),
+ contentType: MediaType.parse(lookupMimeType(fileName)),
+ filename: fileName,
+ ));
+ }
+
+ final rfieldsmap = json.encode(adasd);
+
+ r.fields['map'] = rfieldsmap;
+
+ r.files.addAll(fileList);
+ return r;
+}
+
+HttpHeadersAndBody _selectHttpOptionsAndBody(
Operation operation,
HttpConfig fallbackConfig, [
HttpConfig linkConfig,
@@ -112,7 +200,7 @@ HttpOptionsAndBody _selectHttpOptionsAndBody(
// http options
- // initialze with fallback http options
+ // initialize with fallback http options
http.addAll(fallbackConfig.http);
// inject the configured http options
@@ -127,7 +215,7 @@ HttpOptionsAndBody _selectHttpOptionsAndBody(
// options
- // initialze with fallback options
+ // initialize with fallback options
options.addAll(fallbackConfig.options);
// inject the configured options
@@ -185,22 +273,12 @@ HttpOptionsAndBody _selectHttpOptionsAndBody(
body['query'] = operation.document;
}
- return HttpOptionsAndBody(
- options: options,
- body: encodeBody(body),
+ return HttpHeadersAndBody(
+ headers: options['headers'] as Map<String, String>,
+ body: body,
);
}
-dynamic encodeBody(dynamic body) {
- final encodedBody = json.encode(body, toEncodable: (dynamic object) {
- if(object is File){
- return null;
- }
- return object.toJson();
- });
- return encodedBody;
-}
-
Future<FetchResult> _parseResponse(StreamedResponse response) async {
final int statusCode = response.statusCode;
final String reasonPhrase = response.reasonPhrase;
@@ -208,7 +286,8 @@ Future<FetchResult> _parseResponse(StreamedResponse response) async {
try {
final Encoding encoding = _determineEncodingFromResponse(response);
// @todo limit bodyBytes
- final String decodedBody = encoding.decode(await response.stream.toBytes());
+ final reponseByte = await response.stream.toBytes();
+ final String decodedBody = encoding.decode(reponseByte);
final Map<String, dynamic> jsonResponse =
json.decode(decodedBody) as Map<String, dynamic>;
@@ -231,6 +310,7 @@ Future<FetchResult> _parseResponse(StreamedResponse response) async {
'Network Error: $statusCode $reasonPhrase',
);
}
+ rethrow;
}
}

diff:
import 'dart:convert';
import 'dart:io' show File, Platform;
+import 'dart:typed_data' show Uint8List;
+import 'package:path/path.dart' show dirname, join;
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
import 'package:http/http.dart' as http;
@@ -39,7 +41,7 @@ void main() {
Link link;
GraphQLClient graphQLClientClient;
MockHttpClient mockHttpClient;
-
+ group('simple json', () {
setUp(() {
mockHttpClient = MockHttpClient();
@@ -50,7 +52,7 @@ void main() {
getToken: () async => 'Bearer my-special-bearer-token',
);
- link = authLink.concat(httpLink);
+ link = authLink.concat(httpLink as Link);
graphQLClientClient = GraphQLClient(
cache: NormalizedInMemoryCache(
@@ -124,7 +126,8 @@ void main() {
'Authorization': 'Bearer my-special-bearer-token',
},
);
- expect(await capt.finalize().bytesToString(), r'{"operationName":"ReadRepositories","variables":{"nRepositories":42},"query":" query ReadRepositories($nRepositories: Int!) {\n viewer {\n repositories(last: $nRepositories) {\n nodes {\n __typename\n id\n name\n viewerHasStarred\n }\n }\n }\n }\n"}');
+ expect(await capt.finalize().bytesToString(),
+ r'{"operationName":"ReadRepositories","variables":{"nRepositories":42},"query":" query ReadRepositories($nRepositories: Int!) {\n viewer {\n repositories(last: $nRepositories) {\n nodes {\n __typename\n id\n name\n viewerHasStarred\n }\n }\n }\n }\n"}');
expect(r.errors, isNull);
expect(r.data, isNotNull);
@@ -149,9 +152,7 @@ void main() {
'nRepositories': 38,
},
);
- when(
- mockHttpClient.send(any)
- ).thenAnswer((Invocation a) async {
+ when(mockHttpClient.send(any)).thenAnswer((Invocation a) async {
const String body =
'{"data":{"action":{"starrable":{"viewerHasStarred":true}}}}';
@@ -163,30 +164,33 @@ void main() {
return r;
});
- final QueryResult r = await graphQLClientClient.mutate(_options);
+ final QueryResult response = await graphQLClientClient.mutate(_options);
- final http.Request capt = verify(mockHttpClient.send(captureAny))
+ final http.Request request = verify(mockHttpClient.send(captureAny))
.captured
.first as http.Request;
- expect(capt.method, 'post');
- expect(capt.url.toString(), 'https://api.github.com/graphql');
+ expect(request.method, 'post');
+ expect(request.url.toString(), 'https://api.github.com/graphql');
expect(
- capt.headers,
+ request.headers,
<String, String>{
'accept': '*/*',
'content-type': 'application/json; charset=utf-8',
'Authorization': 'Bearer my-special-bearer-token',
},
);
- expect(await capt.finalize().bytesToString(), r'{"operationName":"AddStar","variables":{"nRepositories":38},"query":" mutation AddStar($starrableId: ID!) {\n action: addStar(input: {starrableId: $starrableId}) {\n starrable {\n viewerHasStarred\n }\n }\n }\n"}');
+ expect(await request.finalize().bytesToString(),
+ r'{"operationName":"AddStar","variables":{"nRepositories":38},"query":" mutation AddStar($starrableId: ID!) {\n action: addStar(input: {starrableId: $starrableId}) {\n starrable {\n viewerHasStarred\n }\n }\n }\n"}');
- expect(r.errors, isNull);
- expect(r.data, isNotNull);
+ expect(response.errors, isNull);
+ expect(response.data, isNotNull);
final bool viewerHasStarred =
- r.data['action']['starrable']['viewerHasStarred'] as bool;
+ response.data['action']['starrable']['viewerHasStarred'] as bool;
expect(viewerHasStarred, true);
});
});
+ });
+
group('upload', () {
const String uploadMutation = r'''
mutation($files: [Upload!]!) {
@@ -197,31 +201,161 @@ void main() {
path
}
}
+ ''';
+
+ setUp(() {
+ mockHttpClient = MockHttpClient();
+
+ httpLink = HttpLink(
+ uri: 'http://localhost:3001/graphql', httpClient: mockHttpClient);
+ authLink = AuthLink(
+ getToken: () async => 'Bearer my-special-bearer-token',
+ );
+
+ link = authLink.concat(httpLink as Link);
+
+ graphQLClientClient = GraphQLClient(
+ cache: NormalizedInMemoryCache(
+ dataIdFromObject: typenameDataIdFromObject,
+ ),
+ link: link,
+ );
+ });
+
+ test('upload success', () async {
+ Future<void> expectUploadBody(http.ByteStream bodyBytesStream,
+ String boundary, List<File> files) async {
+ final List<Function> expectContinuationList = (() {
+ int i = 0;
+ return [
+ // ExpectString
+ (Uint8List actual, String expected) => expect(
+ String.fromCharCodes(actual.sublist(i, i += expected.length)),
+ expected),
+ // ExpectBytes
+ (Uint8List actual, Uint8List expected) =>
+ expect(actual.sublist(i, i += expected.length), expected),
+ // Expect final length
+ (int expectedLength) => expect(i, expectedLength),
+ ];
+ })();
+ final Function expectContinuationString = expectContinuationList[0];
+ final Function expectContinuationBytes = expectContinuationList[1];
+ final Function expectContinuationLength = expectContinuationList[2];
+ final Uint8List bodyBytes = await bodyBytesStream.toBytes();
+ expectContinuationString(bodyBytes, '--');
+ expectContinuationString(bodyBytes, boundary);
+ expectContinuationString(bodyBytes,
+ '\r\ncontent-disposition: form-data; name="operations"\r\n\r\n');
+ expectContinuationString(bodyBytes,
+ r'{"operationName":null,"variables":{"files":[null,null]},"query":" mutation($files: [Upload!]!) {\n multipleUpload(files: $files) {\n id\n filename\n mimetype\n path\n }\n }\n "}');
+ expectContinuationString(bodyBytes, '\r\n--');
+ expectContinuationString(bodyBytes, boundary);
+ expectContinuationString(bodyBytes,
+ '\r\ncontent-disposition: form-data; name="map"\r\n\r\n{"0":["variables.files.0"],"1":["variables.files.1"]}');
+ expectContinuationString(bodyBytes, '\r\n--');
+ expectContinuationString(bodyBytes, boundary);
+ expectContinuationString(bodyBytes,
+ '\r\ncontent-type: image/jpeg\r\ncontent-disposition: form-data; name="0"; filename="sample_upload.jpg"\r\n\r\n');
+ expectContinuationBytes(bodyBytes, await files[0].readAsBytes());
+ expectContinuationString(bodyBytes, '\r\n--');
+ expectContinuationString(bodyBytes, boundary);
+ expectContinuationString(bodyBytes,
+ '\r\ncontent-type: video/quicktime\r\ncontent-disposition: form-data; name="1"; filename="sample_upload.mov"\r\n\r\n');
+ expectContinuationBytes(bodyBytes, await files[1].readAsBytes());
+ expectContinuationString(bodyBytes, '\r\n--');
+ expectContinuationString(bodyBytes, boundary);
+ expectContinuationString(bodyBytes, '--\r\n');
+ expectContinuationLength(bodyBytes.lengthInBytes);
+ }
+
+ http.ByteStream bodyBytes;
+ when(mockHttpClient.send(any)).thenAnswer((Invocation a) async {
+ bodyBytes = (a.positionalArguments[0] as http.BaseRequest).finalize();
+ const String body = r'''
+{
+ "data": {
+ "multipleUpload": [
+ {
+ "id": "r1odc4PAz",
+ "filename": "sample_upload.jpg",
+ "mimetype": "image/jpeg",
+ "path": "./uploads/r1odc4PAz-sample_upload.jpg"
+ },
+ {
+ "id": "5Ea18qlMur",
+ "filename": "sample_upload.mov",
+ "mimetype": "video/quicktime",
+ "path": "./uploads/5Ea18qlMur-sample_upload.mov"
+ }
+ ]
+ }
+}
''';
-// test('upload success', () async {
-// final List<File> files = <String>['sample_upload.bin', 'sample_upload.img']
-// .map((String fileName) => join(
-// dirname(Platform.script.path),
-// 'test',
-// ))
-// .map((String filePath) => File(filePath))
-// .toList();
-//
-// final MutationOptions _options = MutationOptions(
-// document: uploadMutation,
-// variables: <String, dynamic>{
-// 'files': files,
-// },
-// );
-//
-//
-// final QueryResult r = await graphQLClientClient.mutate(_options);
-// expect(r.errors, isNull);
-// expect(r.data, isNotNull);
-// final bool viewerHasStarred =
-// r.data['action']['starrable']['viewerHasStarred'] as bool;
-// expect(viewerHasStarred, true);
-// });
+
+ final List<int> bytes = utf8.encode(body);
+ final Stream<List<int>> stream =
+ Stream<List<int>>.fromIterable(<List<int>>[bytes]);
+
+ final http.StreamedResponse r = http.StreamedResponse(stream, 200);
+ return r;
+ });
+
+ final String basePath = dirname(Platform.script.path);
+ final String realPath =
+ basePath.endsWith('test') ? basePath : join(basePath, 'test');
+ final List<File> files =
+ <String>['sample_upload.jpg', 'sample_upload.mov']
+ .map((String fileName) => join(
+ realPath,
+ fileName,
+ ))
+ .map((String filePath) => File(filePath))
+ .toList();
+
+ final MutationOptions _options = MutationOptions(
+ document: uploadMutation,
+ variables: <String, dynamic>{
+ 'files': files,
+ },
+ );
+ final QueryResult r = await graphQLClientClient.mutate(_options);
+
+ expect(r.errors, isNull);
+ expect(r.data, isNotNull);
+
+ final http.MultipartRequest request =
+ verify(mockHttpClient.send(captureAny)).captured.first
+ as http.MultipartRequest;
+ expect(request.method, 'post');
+ expect(request.url.toString(), 'http://localhost:3001/graphql');
+ expect(request.headers['accept'], '*/*');
+ expect(
+ request.headers['Authorization'], 'Bearer my-special-bearer-token');
+ final List<String> contentTypeStringSplit =
+ request.headers['content-type'].split('; boundary=');
+ expect(contentTypeStringSplit[0], 'multipart/form-data');
+ await expectUploadBody(bodyBytes, contentTypeStringSplit[1], files);
+
+ final List<Map<String, dynamic>> multipleUpload =
+ (r.data['multipleUpload'] as List<dynamic>)
+ .cast<Map<String, dynamic>>();
+
+ expect(multipleUpload, <Map<String, String>>[
+ <String, String>{
+ 'id': 'r1odc4PAz',
+ 'filename': 'sample_upload.jpg',
+ 'mimetype': 'image/jpeg',
+ 'path': './uploads/r1odc4PAz-sample_upload.jpg'
+ },
+ <String, String>{
+ 'id': '5Ea18qlMur',
+ 'filename': 'sample_upload.mov',
+ 'mimetype': 'video/quicktime',
+ 'path': './uploads/5Ea18qlMur-sample_upload.mov'
+ },
+ ]);
+ });
});
}

diff: Binary files /dev/null and b/test/sample_upload.jpg differ

diff: Binary files /dev/null and b/test/sample_upload.mov differ

original_message: Support GraphQL Upload spec as proposed at https://github.com/jaydenseric/graphql-multipart-request-spec (#2)


message: More stringent CI check

diff:
@@ -7,9 +7,9 @@ test_task:
# fingerprint_script: cat pubspec.lock
# populate_script: pub get
pub_script: flutter packages get # later when we seperate graphql_client and graphql_flutter, we can change this
- # format_script: dartfmt **/*.dart -n --set-exit-if-changed
+ format_script: dartfmt **/*.dart -n --set-exit-if-changed
# analyze_script: dartanalyzer --fatal-infos .
- # publishable_script: pub publish --dry-run
+ publishable_script: pub publish --dry-run
environment:
CODECOV_TOKEN: ENCRYPTED[fa57a1dda8bca6ad45ef10043f632b68b7824b60c634415f741a847788c1a44ae9374155e3af3346fb9b6d72bac0e1d3]
test_script:

diff:
-## [1.0.0-beta.1] - February 16 2019
+## [1.0.0-beta.1+1] - February 16 2019
We are finally in BETA. This means we're one step closer to our first stable release.
Thanks to all the contributes.
+Support GraphQL Upload spec as proposed at
+https://github.com/jaydenseric/graphql-multipart-request-spec
+
### What's changed?
We have added a brand new `Link` that handles authentication. You can drop it in like so:

diff:
@@ -22,7 +22,7 @@ class Bloc {
_toggleStarSubject.listen((Repo t) async {
_toggleStarLoadingSubject.add(true);
// @todo handle error
- final r = await _mutateToggleStar(t);
+ final QueryResult _ = await _mutateToggleStar(t);
_repoSubject.add(_repoSubject.value.map((Repo e) {
if (e.id != t.id) {
@@ -67,7 +67,7 @@ class Bloc {
link: _link,
);
- Future<void> _mutateToggleStar(Repo repo) async {
+ Future<QueryResult> _mutateToggleStar(Repo repo) async {
final MutationOptions _options = MutationOptions(
document:
repo.viewerHasStarred ? mutations.removeStar : mutations.addStar,

diff:
@@ -5,7 +5,7 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
- rxdart: 0.21.0
+ rxdart: ^0.21.0
graphql_flutter:
path: ..

diff:
@@ -38,7 +38,9 @@ class ObservableQuery {
final QueryManager queryManager;
final Set<StreamSubscription<QueryResult>> _onDataSubscriptions =
- <StreamSubscription<QueryResult>>{};
+ // @todo Set literal is only supported from Dart 2.2 we are running Dart 2.0
+ // ignore: prefer_collection_literals
+ Set<StreamSubscription<QueryResult>>();
QueryLifecycle lifecycle = QueryLifecycle.UNEXECUTED;

diff:
import 'dart:async';
import 'dart:convert';
import 'dart:io';
+import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:http/http.dart';
@@ -25,6 +26,8 @@ class HttpLink extends Link {
Map<String, dynamic> credentials,
Map<String, dynamic> fetchOptions,
}) : super(
+ // @todo possibly this is a bug in dart analyzer
+ // ignore: undefined_named_parameter
request: (
Operation operation, [
NextLink forward,
@@ -105,7 +108,7 @@ Map<String, File> _getFileMap(
}) {
currentMap ??= <String, File>{};
if (body is Map<String, dynamic>) {
- final entries = body.entries;
+ final Iterable<MapEntry<String, dynamic>> entries = body.entries;
for (MapEntry<String, dynamic> element in entries) {
currentMap.addAll(_getFileMap(
element.value,
@@ -154,18 +157,17 @@ Future<BaseRequest> _prepareRequest(
return object.toJson();
});
- // @todo fileMap.keys.toList() and fileMap.values.toList() same order????
- final Map<String, List<String>> adasd =
- {}; // fileMap.keys.toList().asMap().map((int index, String filePath) => MapEntry(index.toString(),[filePath]));
- final List<MultipartFile> fileList = [];
+ final Map<String, List<String>> fileMapping = <String, List<String>>{};
+ final List<MultipartFile> fileList = <MultipartFile>[];
- final fEn = fileMap.entries.toList(growable: false);
+ final List<MapEntry<String, File>> fileMapEntries =
+ fileMap.entries.toList(growable: false);
- for (int i = 0; i < fEn.length; i++) {
- final MapEntry<String, File> entry = fEn[i];
+ for (int i = 0; i < fileMapEntries.length; i++) {
+ final MapEntry<String, File> entry = fileMapEntries[i];
final String indexString = i.toString();
- adasd.addAll({
- indexString: [entry.key]
+ fileMapping.addAll(<String, List<String>>{
+ indexString: <String>[entry.key],
});
final File f = entry.value;
final String fileName = basename(f.path);
@@ -178,9 +180,7 @@ Future<BaseRequest> _prepareRequest(
));
}
- final rfieldsmap = json.encode(adasd);
-
- r.fields['map'] = rfieldsmap;
+ r.fields['map'] = json.encode(fileMapping);
r.files.addAll(fileList);
return r;
@@ -286,7 +286,7 @@ Future<FetchResult> _parseResponse(StreamedResponse response) async {
try {
final Encoding encoding = _determineEncodingFromResponse(response);
// @todo limit bodyBytes
- final reponseByte = await response.stream.toBytes();
+ final Uint8List reponseByte = await response.stream.toBytes();
final String decodedBody = encoding.decode(reponseByte);
final Map<String, dynamic> jsonResponse =

diff:
@@ -11,13 +11,15 @@ dependencies:
sdk: flutter
meta: ^1.1.6
http: ^0.12.0
- mime: 0.9.6+2
+ mime: ^0.9.6+2
+ path: ^1.6.2
http_parser: ^3.1.3
path_provider: ^0.5.0
uuid: ^2.0.0
graphql_parser: ^1.1.1
dev_dependencies:
+ rxdart: ^0.21.0 # for `example/` only to please pub publish --dry-run
perfectionist: ^2.0.0
mockito: ^4.0.0
flutter_test:

diff:
@@ -105,7 +105,7 @@ void main() {
final List<int> bytes = utf8.encode(body);
final Stream<List<int>> stream =
- Stream<List<int>>.fromIterable([bytes]);
+ Stream<List<int>>.fromIterable(<List<int>>[bytes]);
final http.StreamedResponse r = http.StreamedResponse(stream, 200);
@@ -228,7 +228,7 @@ void main() {
String boundary, List<File> files) async {
final List<Function> expectContinuationList = (() {
int i = 0;
- return [
+ return <Function>[
// ExpectString
(Uint8List actual, String expected) => expect(
String.fromCharCodes(actual.sublist(i, i += expected.length)),

original_message: More stringent CI check (#3)


diff: +const String YOUR_PERSONAL_ACCESS_TOKEN = '';

change_type: RENAME

diff:
import 'package:rxdart/subjects.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
-import './mutations/mutations.dart' as mutations;
-import './queries/readRepositories.dart' as queries;
-
-
-const String YOUR_PERSONAL_ACCESS_TOKEN =
- '';
+import '../config.dart' show YOUR_PERSONAL_ACCESS_TOKEN;
+import '../graphql_operation/mutations/mutations.dart' as mutations;
+import '../graphql_operation/queries/readRepositories.dart' as queries;
class Repo {
const Repo({this.id, this.name, this.viewerHasStarred});

diff:
+import 'package:flutter/material.dart';
+
+import 'bloc.dart' show Bloc, Repo;
+
+class GraphQLBlocPatternScreen extends StatefulWidget {
+ GraphQLBlocPatternScreen({
+ Key key,
+ this.title = 'GraphQL Widget',
+ }) : bloc = Bloc(),
+ super(key: key);
+
+ final String title;
+ final Bloc bloc;
+
+ @override
+ _MyHomePageState createState() => _MyHomePageState(bloc);
+}
+
+class _MyHomePageState extends State<GraphQLBlocPatternScreen> {
+ _MyHomePageState(this.bloc);
+ final Bloc bloc;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(widget.title),
+ ),
+ body: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: <Widget>[
+ TextField(
+ decoration: const InputDecoration(
+ labelText: 'Number of repositories (default 50)',
+ ),
+ keyboardType: TextInputType.number,
+ onChanged: (String n) =>
+ bloc.updateNumberOfRepoSink.add(int.parse(n)),
+ ),
+ StreamBuilder<List<Repo>>(
+ stream: bloc.repoStream,
+ builder: (BuildContext context,
+ AsyncSnapshot<List<Repo>> snapshot) {
+ if (snapshot.hasError) {
+ return Text('\nErrors: \n ' +
+ (snapshot.error as List<dynamic>).join(',\n '));
+ }
+ if (snapshot.data == null) {
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+
+ final List<Repo> repositories = snapshot.data;
+
+ return Expanded(
+ child: ListView.builder(
+ itemCount: repositories.length,
+ itemBuilder: (BuildContext context, int index) =>
+ StarrableRepository(
+ repository: repositories[index], bloc: bloc),
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class StarrableRepository extends StatelessWidget {
+ const StarrableRepository({
+ Key key,
+ @required this.repository,
+ @required this.bloc,
+ }) : super(key: key);
+
+ final Bloc bloc;
+ final Repo repository;
+
+ Map<String, Object> extractRepositoryData(Map<String, Object> data) {
+ final Map<String, Object> action = data['action'] as Map<String, Object>;
+
+ if (action == null) {
+ return null;
+ }
+
+ return action['starrable'] as Map<String, Object>;
+ }
+
+ bool get viewerHasStarred => repository.viewerHasStarred;
+
+ @override
+ Widget build(BuildContext context) {
+ return StreamBuilder<bool>(
+ stream: bloc.toggleStarLoadingStream,
+ initialData: false,
+ builder: (BuildContext context, AsyncSnapshot<bool> result) {
+ final bool loading = result.data;
+ return ListTile(
+ leading: viewerHasStarred
+ ? const Icon(
+ Icons.star,
+ color: Colors.amber,
+ )
+ : const Icon(Icons.star_border),
+ trailing: loading ? const CircularProgressIndicator() : null,
+ title: Text(repository.name),
+ onTap: () {
+ bloc.toggleStarSink.add(repository);
+ },
+ );
+ },
+ );
+ }
+}

diff:
+import 'package:flutter/material.dart';
+import 'package:graphql_flutter/graphql_flutter.dart';
+
+import '../config.dart' show YOUR_PERSONAL_ACCESS_TOKEN;
+import '../graphql_operation/mutations/mutations.dart' as mutations;
+import '../graphql_operation/queries/readRepositories.dart' as queries;
+
+class GraphQLWidgetScreen extends StatelessWidget {
+ const GraphQLWidgetScreen(): super();
+ @override
+ Widget build(BuildContext context) {
+ final HttpLink httpLink = HttpLink(
+ uri: 'https://api.github.com/graphql',
+ );
+
+ final AuthLink authLink = AuthLink(
+ getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
+ );
+
+ final Link link = authLink.concat(httpLink as Link);
+
+ final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
+ GraphQLClient(
+ cache: NormalizedInMemoryCache(
+ dataIdFromObject: typenameDataIdFromObject,
+ ),
+ link: link,
+ ),
+ );
+
+ return GraphQLProvider(
+ client: client,
+ child: const CacheProvider(
+ child: MyHomePage(title: 'GraphQL Widget'),
+ ),
+ );
+ }
+}
+
+class MyHomePage extends StatefulWidget {
+ const MyHomePage({
+ Key key,
+ this.title,
+ }) : super(key: key);
+
+ final String title;
+
+ @override
+ _MyHomePageState createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+ int nRepositories = 50;
+
+ void changeQuery(String number) {
+ setState(() {
+ nRepositories = int.parse(number) ?? 50;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(widget.title),
+ ),
+ body: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: <Widget>[
+ TextField(
+ decoration: const InputDecoration(
+ labelText: 'Number of repositories (default 50)',
+ ),
+ keyboardType: TextInputType.number,
+ onSubmitted: changeQuery,
+ ),
+ Query(
+ options: QueryOptions(
+ document: queries.readRepositories,
+ variables: <String, dynamic>{
+ 'nRepositories': nRepositories,
+ },
+ pollInterval: 4,
+ ),
+ builder: (QueryResult result) {
+ if (result.loading) {
+ return const Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+
+ if (result.hasErrors) {
+ return Text('\nErrors: \n ' + result.errors.join(',\n '));
+ }
+
+ if (result.data == null && result.errors == null) {
+ return const Text('Both data and errors are null, this is a known bug after refactoring, you might forget to set Github token');
+ }
+
+ // result.data can be either a [List<dynamic>] or a [Map<String, dynamic>]
+ final List<dynamic> repositories = result.data['viewer']
+ ['repositories']['nodes'] as List<dynamic>;
+
+ return Expanded(
+ child: ListView.builder(
+ itemCount: repositories.length,
+ itemBuilder: (BuildContext context, int index) =>
+ StarrableRepository(
+ repository:
+ repositories[index] as Map<String, Object>),
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class StarrableRepository extends StatefulWidget {
+ const StarrableRepository({
+ Key key,
+ @required this.repository,
+ }) : super(key: key);
+
+ final Map<String, Object> repository;
+
+ @override
+ StarrableRepositoryState createState() {
+ return StarrableRepositoryState();
+ }
+}
+
+class StarrableRepositoryState extends State<StarrableRepository> {
+ bool loading = false;
+
+ Map<String, Object> extractRepositoryData(Map<String, Object> data) {
+ final Map<String, Object> action = data['action'] as Map<String, Object>;
+
+ if (action == null) {
+ return null;
+ }
+
+ return action['starrable'] as Map<String, Object>;
+ }
+
+ bool get viewerHasStarred => widget.repository['viewerHasStarred'] as bool;
+
+ @override
+ Widget build(BuildContext context) {
+ final bool starred = loading ? !viewerHasStarred : viewerHasStarred;
+
+ return Mutation(
+ key: Key(starred.toString()),
+ options: MutationOptions(
+ document: starred ? mutations.removeStar : mutations.addStar,
+ ),
+ builder: (RunMutation toggleStar, QueryResult result) {
+ return ListTile(
+ leading: starred
+ ? const Icon(
+ Icons.star,
+ color: Colors.amber,
+ )
+ : const Icon(Icons.star_border),
+ trailing: loading ? const CircularProgressIndicator() : null,
+ title: Text(widget.repository['name'] as String),
+ onTap: () {
+ // optimistic ui updates are not implemented yet,
+ // so we track loading manually
+ setState(() {
+ loading = true;
+ });
+ toggleStar(<String, dynamic>{
+ 'starrableId': widget.repository['id'],
+ });
+ },
+ );
+ },
+ update: (Cache cache, QueryResult result) {
+ if (result.hasErrors) {
+ print(result.errors);
+ } else {
+ final Map<String, Object> updated = Map<String, Object>.from(
+ widget.repository)
+ ..addAll(extractRepositoryData(result.data as Map<String, Object>));
+
+ cache.write(typenameDataIdFromObject(updated), updated);
+ }
+ },
+ onCompleted: (QueryResult result) {
+ showDialog<AlertDialog>(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text(
+ extractRepositoryData(result.data as Map<String, Object>)[
+ 'viewerHasStarred'] as bool
+ ? 'Thanks for your star!'
+ : 'Sorry you changed your mind!',
+ ),
+ actions: <Widget>[
+ SimpleDialogOption(
+ child: const Text('Dismiss'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ )
+ ],
+ );
+ },
+ );
+ setState(() {
+ loading = false;
+ });
+ },
+ );
+ }
+}

diff:
import 'package:flutter/material.dart';
+import './graphql_bloc/main.dart' show GraphQLBlocPatternScreen;
+import './graphql_widget/main.dart' show GraphQLWidgetScreen;
-import 'bloc.dart' show Bloc, Repo;
-
-void main() => runApp(MyApp());
-
-class MyApp extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return MaterialApp(
+void main() => runApp(
+ MaterialApp(
title: 'GraphQL Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
- home: MyHomePage(title: 'GraphQL Flutter Home Page'),
- );
- }
-}
-
-class MyHomePage extends StatefulWidget {
- MyHomePage({
- Key key,
- this.title,
- }) : bloc = Bloc(),
- super(key: key);
-
- final String title;
- final Bloc bloc;
-
- @override
- _MyHomePageState createState() => _MyHomePageState(bloc);
-}
-
-class _MyHomePageState extends State<MyHomePage> {
- _MyHomePageState(this.bloc);
- final Bloc bloc;
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
+ home: Builder(
+ builder: (BuildContext context) => Scaffold(
appBar: AppBar(
- title: Text(widget.title),
+ title: const Text('GraphQL Demo App'),
),
- body: Container(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ body: Center(
child: Column(
- mainAxisAlignment: MainAxisAlignment.start,
- mainAxisSize: MainAxisSize.max,
children: <Widget>[
- TextField(
- decoration: const InputDecoration(
- labelText: 'Number of repositories (default 50)',
- ),
- keyboardType: TextInputType.number,
- onChanged: (String n) =>
- bloc.updateNumberOfRepoSink.add(int.parse(n)),
+ RaisedButton(
+ child: const Text('GraphQL BloC pattern'),
+ onPressed: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute<GraphQLWidgetScreen>(
+ builder: (BuildContext context) =>
+ GraphQLBlocPatternScreen(),
),
- StreamBuilder<List<Repo>>(
- stream: bloc.repoStream,
- builder: (BuildContext context,
- AsyncSnapshot<List<Repo>> snapshot) {
- if (snapshot.hasError) {
- return Text('\nErrors: \n ' +
- (snapshot.error as List<dynamic>).join(',\n '));
- }
- if (snapshot.data == null) {
- return const Center(
- child: CircularProgressIndicator(),
);
- }
-
- final List<Repo> repositories = snapshot.data;
-
- return Expanded(
- child: ListView.builder(
- itemCount: repositories.length,
- itemBuilder: (BuildContext context, int index) =>
- StarrableRepository(
- repository: repositories[index], bloc: bloc),
+ },
+ ),
+ RaisedButton(
+ child: const Text('GraphQL Widget'),
+ onPressed: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute<GraphQLWidgetScreen>(
+ builder: (BuildContext context) =>
+ const GraphQLWidgetScreen(),
),
);
},
@@ -84,53 +43,7 @@ class _MyHomePageState extends State<MyHomePage> {
],
),
),
+ ),
+ ),
+ ),
);
- }
-}
-
-class StarrableRepository extends StatelessWidget {
- const StarrableRepository({
- Key key,
- @required this.repository,
- @required this.bloc,
- }) : super(key: key);
-
- final Bloc bloc;
- final Repo repository;
-
- Map<String, Object> extractRepositoryData(Map<String, Object> data) {
- final Map<String, Object> action = data['action'] as Map<String, Object>;
-
- if (action == null) {
- return null;
- }
-
- return action['starrable'] as Map<String, Object>;
- }
-
- bool get viewerHasStarred => repository.viewerHasStarred;
-
- @override
- Widget build(BuildContext context) {
- return StreamBuilder<bool>(
- stream: bloc.toggleStarLoadingStream,
- initialData: false,
- builder: (BuildContext context, AsyncSnapshot<bool> result) {
- final bool loading = result.data;
- return ListTile(
- leading: viewerHasStarred
- ? const Icon(
- Icons.star,
- color: Colors.amber,
- )
- : const Icon(Icons.star_border),
- trailing: loading ? const CircularProgressIndicator() : null,
- title: Text(repository.name),
- onTap: () {
- bloc.toggleStarSink.add(repository);
- },
- );
- },
- );
- }
-}

original_message: separate BLoC and widget pattern, known bug that graphql error is not return (#4)


message: fix bug of empty error

diff:
@@ -281,13 +281,11 @@ HttpHeadersAndBody _selectHttpOptionsAndBody(
Future<FetchResult> _parseResponse(StreamedResponse response) async {
final int statusCode = response.statusCode;
- final String reasonPhrase = response.reasonPhrase;
- try {
final Encoding encoding = _determineEncodingFromResponse(response);
// @todo limit bodyBytes
- final Uint8List reponseByte = await response.stream.toBytes();
- final String decodedBody = encoding.decode(reponseByte);
+ final Uint8List responseByte = await response.stream.toBytes();
+ final String decodedBody = encoding.decode(responseByte);
final Map<String, dynamic> jsonResponse =
json.decode(decodedBody) as Map<String, dynamic>;
@@ -301,17 +299,16 @@ Future<FetchResult> _parseResponse(StreamedResponse response) async {
fetchResult.data = jsonResponse['data'];
}
- return fetchResult;
- } catch (e) {
- // @todo get body as well;
- // final String decodedBody = encoding.decode(response.bodyBytes);
+ if (fetchResult.data == null && fetchResult.errors == null) {
if (statusCode < 200 || statusCode >= 400) {
throw ClientException(
- 'Network Error: $statusCode $reasonPhrase',
+ 'Network Error: $statusCode $decodedBody',
);
}
- rethrow;
+ throw ClientException('Invalid response body: $decodedBody');
}
+
+ return fetchResult;
}
/// Returns the charset encoding for the given response.

diff:
@@ -4,6 +4,7 @@ version: 1.0.0-beta.1+1
authors:
- Eus Dima <eus@zinoapp.com>
- Zino Hofmann <zino@zinoapp.com>
+ - TruongSinh Tran-Nguyen <i@truongsinh.pro>
homepage: https://github.com/zino-app/graphql-flutter/tree/next
dependencies:

diff:
@@ -142,7 +142,12 @@ void main() {
});
// test('failed query because of network', {});
// test('failed query because of because of error response', {});
-// test('failed query because of because of invalid response', {});
+// test('failed query because of because of invalid response', () {
+// String responseBody =
+// '{\"message\":\"Bad credentials\",\"documentation_url\":\"https://developer.github.com/v4\"}';
+// int responseCode = 401;
+// });
+// test('partially success query with some errors', {});
});
group('mutation', () {
test('successful mutation', () async {
@@ -357,5 +362,11 @@ void main() {
},
]);
});
+
+// test('upload fail error response', () {
+// const String responseBody =
+// r'{"errors":[{"message":"Variable \"$files\" of required type \"[Upload!]!\" was not provided.","locations":[{"line":1,"column":14}],"extensions":{"code":"INTERNAL_SERVER_ERROR","exception":{"stacktrace":["GraphQLError: Variable \"$files\" of required type \"[Upload!]!\" was not provided."," at getVariableValues (/Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/graphql/execution/values.js:76:21)"," at buildExecutionContext (/Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/graphql/execution/execute.js:196:63)"," at executeImpl (/Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/graphql/execution/execute.js:70:20)"," at Object.execute (/Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/graphql/execution/execute.js:62:35)"," at /Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/apollo-server-core/dist/requestPipeline.js:195:36"," at Generator.next (<anonymous>)"," at /Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/apollo-server-core/dist/requestPipeline.js:7:71"," at new Promise (<anonymous>)"," at __awaiter (/Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/apollo-server-core/dist/requestPipeline.js:3:12)"," at execute (/Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/apollo-server-core/dist/requestPipeline.js:179:20)"," at Object.<anonymous> (/Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/apollo-server-core/dist/requestPipeline.js:131:35)"," at Generator.next (<anonymous>)"," at fulfilled (/Users/truongsinh/Dev/flutter/graphql-flutter/example/server/api/node_modules/apollo-server-core/dist/requestPipeline.js:4:58)"," at process._tickCallback (internal/process/next_tick.js:68:7)"]}}}]';
+// const int statusCode = 400;
+// });
});
}

original_message: fix bug of empty error (#5)


diff:
@@ -119,10 +119,7 @@ class QueryManager {
).first;
// save the data from fetchResult to the cache
- if (
- // should never cache a mutation
- !(options is MutationOptions) &&
- fetchResult.data != null &&
+ if (fetchResult.data != null &&
options.fetchPolicy != FetchPolicy.noCache) {
cache.write(
operation.toKey(),

diff:
-import 'dart:convert';
+import 'dart:collection' show SplayTreeMap;
+import 'dart:convert' show json;
+import 'dart:io' show File;
class Operation {
- Operation({
+ factory Operation({
+ String document,
+ Map<String, dynamic> variables,
+ String operationName,
+ Map<String, dynamic> extensions,
+ }) =>
+ Operation._(
+ document: document,
+ variables: SplayTreeMap<String, dynamic>.of(variables),
+ operationName: operationName,
+ extensions: extensions,
+ );
+
+ Operation._({
this.document,
this.variables,
this.operationName,
@@ -9,13 +24,13 @@ class Operation {
});
final String document;
- final Map<String, dynamic> variables;
+ final SplayTreeMap<String, dynamic> variables;
final String operationName;
final Map<String, dynamic> extensions;
final Map<String, dynamic> _context = <String, dynamic>{};
- /// Sets the context of an opration by merging the new context with the old one.
+ /// Sets the context of an operation by merging the new context with the old one.
void setContext(Map<String, dynamic> next) {
_context.addAll(next);
}
@@ -28,9 +43,14 @@ class Operation {
}
String toKey() {
- /// XXX we're assuming here that variables will be serialized in the same order.
- /// that might not always be true
- final String encodedVariables = json.encode(variables);
+ /// SplayTreeMap is always sorted
+ final String encodedVariables =
+ json.encode(variables, toEncodable: (dynamic object) {
+ if (object is File) {
+ return object.path;
+ }
+ return object;
+ });
return '$document|$encodedVariables|$operationName';
}

original_message: mutation is also cached, with SplayTreeMap (#7)


message: fix incomplete restore

diff:
@@ -4,7 +4,6 @@ import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
-
import 'package:graphql_flutter/src/cache/cache.dart';
import 'package:graphql_flutter/src/utilities/helpers.dart'
show deeplyMergeLeft;
@@ -38,11 +37,11 @@ class InMemoryCache implements Cache {
if (_inMemoryCache.containsKey(key) &&
_inMemoryCache[key] is Map &&
value != null &&
- value is Map) {
+ value is Map<String, dynamic>) {
// Avoid overriding a superset with a subset of a field (#155)
// this means deletions must be done by explicitly returning a field as null
_inMemoryCache[key] = deeplyMergeLeft(<Map<String, dynamic>>[
- _inMemoryCache[key],
+ _inMemoryCache[key] as Map<String, dynamic>,
value,
]);
} else {
@@ -102,6 +101,8 @@ class InMemoryCache implements Cache {
sink.writeln(json.encode(<dynamic>[key, value]));
});
+ // before calling sink.close() we should call sink.flush()
+ await sink.flush();
await sink.close();
_writingToStorage = false;
@@ -114,6 +115,18 @@ class InMemoryCache implements Cache {
}
Future<HashMap<String, dynamic>> _readFromStorage() async {
+ if (_writingToStorage) {
+ // On android the app will fire 'inactive' and then 'restored'
+ // when it is restored. Then, while this is reading it may read
+ // incomplete data because the writer may be writing data.
+ // And if it is writing the file was overwritten anyway, so we don't
+ // have what to read and best hope that the data we have in memory
+ // is the correct one.
+ // TODO: should we call save when the app enters inactive (why?)
+ // When the app is 'cold-started' then save won't be called and
+ // restore will run ok.
+ return _inMemoryCache;
+ }
try {
final File file = await _localStorageFile;
final HashMap<String, dynamic> storedHashMap = HashMap<String, dynamic>();

original_message: fix incomplete restore


message: fix(analyze): Fix the analizer issues

diff:
@@ -4,9 +4,7 @@ import 'package:graphql_flutter/graphql_flutter.dart';
import '../graphql_operation/mutations/mutations.dart' as mutations;
import '../graphql_operation/queries/readRepositories.dart' as queries;
-/// Create a ../local.dart file with YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN>'
-/// to make the example work
-import '../local.dart' show YOUR_PERSONAL_ACCESS_TOKEN;
+const String YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN>';
class Repo {
const Repo({this.id, this.name, this.viewerHasStarred});

diff:
@@ -4,11 +4,9 @@ import 'package:graphql_flutter/graphql_flutter.dart';
import '../graphql_operation/mutations/mutations.dart' as mutations;
import '../graphql_operation/queries/readRepositories.dart' as queries;
-/// Create a ../local.dart file with YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN>'
-/// to make the example work
-import '../local.dart' show YOUR_PERSONAL_ACCESS_TOKEN;
+const String YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN>';
-final bool ENABLE_WEBSOCKETS = false;
+const bool ENABLE_WEBSOCKETS = false;
class GraphQLWidgetScreen extends StatelessWidget {
const GraphQLWidgetScreen() : super();
@@ -186,7 +184,7 @@ class StarrableRepository extends StatelessWidget {
document: starred ? mutations.removeStar : mutations.addStar,
),
builder: (RunMutation toggleStar, QueryResult result) {
- print([result.loading, optimistic]);
+ print(<bool>[result.loading, optimistic]);
return ListTile(
leading: starred
? const Icon(

diff:
@@ -126,7 +126,7 @@ class InMemoryCache implements Cache {
}
try {
final File file = await _localStorageFile;
- final storedHashMap = HashMap<String, dynamic>();
+ final HashMap<String, dynamic> storedHashMap = HashMap<String, dynamic>();
if (file.existsSync()) {
final Stream<List<int>> inputStream = file.openRead();

diff:
import 'dart:core';
-import 'dart:collection';
import 'package:meta/meta.dart';
@@ -13,7 +12,7 @@ class LazyCacheMap extends LazyDereferencingMap {
Map<String, Object> data, {
@required Dereference dereference,
CacheState cacheState,
- }) : this.cacheState =
+ }) : cacheState =
cacheState ?? (data is LazyCacheMap ? data.cacheState : null),
super(data, dereference: dereference);

diff:
@@ -146,9 +146,9 @@ class OptimisticCache extends NormalizedInMemoryCache {
patch.id == removeId || _parentPatchId(patch.id) == removeId,
);
- print([
+ print(<dynamic>[
optimisticPatches.length,
- optimisticPatches.map((p) => p.id),
+ optimisticPatches.map<String>((OptimisticPatch p) => p.id),
removeId
]);
}

diff:
@@ -13,7 +13,7 @@ class RawOperationData {
Map<String, dynamic> variables,
String operationName,
}) : _operationName = operationName,
- this.variables = SplayTreeMap<String, dynamic>.of(
+ variables = SplayTreeMap<String, dynamic>.of(
variables ?? const <String, dynamic>{},
);

diff:
@@ -86,7 +86,9 @@ class SocketClient {
///
/// If this instance is disposed, this method does nothing.
Future<void> _connect() async {
- if (_connectionStateController.isClosed) return;
+ if (_connectionStateController.isClosed) {
+ return;
+ }
_connectionStateController.value = SocketConnectionState.CONNECTING;
print('Connecting to websocket: $url...');
@@ -105,15 +107,17 @@ class SocketClient {
.map<GraphQLSocketMessage>(_parseSocketMessage);
if (config.inactivityTimeout != null) {
- _keepAliveSubscription = _connectionKeepAlive
- .timeout(config.inactivityTimeout, onTimeout: (event) {
+ _keepAliveSubscription = _connectionKeepAlive.timeout(
+ config.inactivityTimeout,
+ onTimeout: (EventSink<ConnectionKeepAlive> event) {
print(
"Haven't received keep alive message for ${config.inactivityTimeout.inSeconds} seconds. Disconnecting..");
event.close();
_socket.close(WebSocketStatus.goingAway);
_connectionStateController.value =
SocketConnectionState.NOT_CONNECTED;
- }).listen(null);
+ },
+ ).listen(null);
}
_messageSubscription = _messageStream.listen(
@@ -139,7 +143,9 @@ class SocketClient {
_keepAliveSubscription?.cancel();
_messageSubscription?.cancel();
- if (_connectionStateController.isClosed) return;
+ if (_connectionStateController.isClosed) {
+ return;
+ }
if (_connectionStateController.value != SocketConnectionState.NOT_CONNECTED)
_connectionStateController.value = SocketConnectionState.NOT_CONNECTED;
@@ -149,9 +155,12 @@ class SocketClient {
print(
'Scheduling to connect in ${config.delayBetweenReconnectionAttempts.inSeconds} seconds...');
- _reconnectTimer = Timer(config.delayBetweenReconnectionAttempts, () {
+ _reconnectTimer = Timer(
+ config.delayBetweenReconnectionAttempts,
+ () {
_connect();
- });
+ },
+ );
} else {
Timer.run(() => _connect());
}
@@ -238,35 +247,51 @@ class SocketClient {
.take(1);
final Observable<SocketConnectionState> waitForConnectedState = addTimeout
- ? waitForConnectedStateWithoutTimeout
- .timeout(config.queryAndMutationTimeout, onTimeout: (e) {
+ ? waitForConnectedStateWithoutTimeout.timeout(
+ config.queryAndMutationTimeout,
+ onTimeout: (EventSink<SocketConnectionState> event) {
print('Connection timed out.');
response.addError(TimeoutException('Connection timed out.'));
- e.close();
+ event.close();
response.close();
- })
+ },
+ )
: waitForConnectedStateWithoutTimeout;
sub = waitForConnectedState.listen((_) {
final Stream<GraphQLSocketMessage> dataErrorComplete =
- _messageStream.where((GraphQLSocketMessage message) {
- if (message is SubscriptionData) return message.id == id;
- if (message is SubscriptionError) return message.id == id;
- if (message is SubscriptionComplete) return message.id == id;
+ _messageStream.where(
+ (GraphQLSocketMessage message) {
+ if (message is SubscriptionData) {
+ return message.id == id;
+ }
+
+ if (message is SubscriptionError) {
+ return message.id == id;
+ }
+
+ if (message is SubscriptionComplete) {
+ return message.id == id;
+ }
+
return false;
- }).takeWhile((_) => !response.isClosed);
+ },
+ ).takeWhile((_) => !response.isClosed);
final Stream<GraphQLSocketMessage> subscriptionComplete = addTimeout
? dataErrorComplete
.where((GraphQLSocketMessage message) =>
message is SubscriptionComplete)
.take(1)
- .timeout(config.queryAndMutationTimeout, onTimeout: (e) {
+ .timeout(
+ config.queryAndMutationTimeout,
+ onTimeout: (EventSink<GraphQLSocketMessage> event) {
print('Request timed out.');
response.addError(TimeoutException('Request timed out.'));
- e.close();
+ event.close();
response.close();
- })
+ },
+ )
: dataErrorComplete
.where((GraphQLSocketMessage message) =>
message is SubscriptionComplete)

original_message: fix(analyze): Fix the analizer issues


message: fix: match badges

diff:
-# GraphQL Flutter <!-- omit in toc -->
+# GraphQL Flutter
[![Build Status][build-status-badge]][build-status-link]
[![Coverage][coverage-badge]][coverage-link]
[![version][version-badge]][package-link]
[![MIT License][license-badge]][license-link]
-[![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg?style=flat-square)](#contributors)
+[![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg)](#contributors)
[![PRs Welcome][prs-badge]](http://makeapullrequest.com)
[![Watch on GitHub][github-watch-badge]][github-watch]
[![Star on GitHub][github-star-badge]][github-star]
-## Table of Contents <!-- omit in toc -->
+## Table of Contents
- [About this project](#about-this-project)
- [Installation](#installation)
@@ -613,11 +613,11 @@ This project follows the [all-contributors](https://github.com/kentcdodds/all-co
[build-status-link]: https://cirrus-ci.com/github/truongsinh/dart-uuid/master
[coverage-badge]: https://codecov.io/gh/truongsinh/graphql-flutter/branch/master/graph/badge.svg
[coverage-link]: https://codecov.io/gh/truongsinh/graphql-flutter
-[version-badge]: https://img.shields.io/pub/v/graphql_flutter.svg?style=flat-square
+[version-badge]: https://img.shields.io/pub/v/graphql_flutter.svg
[package-link]: https://pub.dartlang.org/packages/graphql_flutter/versions/1.0.0-alpha.3
-[license-badge]: https://img.shields.io/github/license/zino-app/graphql-flutter.svg?style=flat-square
+[license-badge]: https://img.shields.io/github/license/zino-app/graphql-flutter.svg
[license-link]: https://github.com/zino-app/graphql-flutter/blob/master/LICENSE
-[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
+[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
[prs]: http://makeapullrequest.com
[github-watch-badge]: https://img.shields.io/github/watchers/zino-app/graphql-flutter.svg?style=social
[github-watch]: https://github.com/zino-app/graphql-flutter/watchers

diff:
-# app
+# Example App
A new Flutter project.

diff:
name: graphql_flutter
description: A GraphQL client for Flutter, bringing all the features from a modern GraphQL client to one easy to use package.
-version: 1.0.0
+version: 1.0.0+1
authors:
- Eus Dima <eus@zinoapp.com>
- Zino Hofmann <zino@zinoapp.com>

original_message: fix: match badges


message: chore: implement ci and cd

diff:
+version: 2.1
+aliases:
+ - &docker_environment
+ - image: cirrusci/flutter:latest
+ - &restore_cache
+ keys:
+ # when lock file changes, use increasingly general patterns to restore cache
+ - pub-packages-v1-{{ checksum "packages/graphql/pubspec.lock" }}-{{ checksum "packages/graphql_flutter/pubspec.lock" }}
+ - pub-packages-v1-{{ checksum "packages/graphql/pubspec.lock" }}
+ - pub-packages-v1-
+jobs:
+ dependencies:
+ docker: *docker_environment
+ steps:
+ - checkout
+ - restore_cache: *restore_cache
+ - run:
+ name: Install dependencies (graphql)
+ command: |
+ cd packages/graphql
+ flutter packages get
+ - run:
+ name: Install dependencies (graphql_flutter)
+ command: |
+ cd packages/graphql_flutter
+ flutter packages get
+ - save_cache:
+ key: pub-packages-v1-{{ checksum "packages/graphql/pubspec.lock" }}-{{ checksum "packages/graphql_flutter/pubspec.lock" }}
+ paths:
+ - ~/.pub-cache
+ lint:
+ docker: *docker_environment
+ steps:
+ - checkout
+ - restore_cache: *restore_cache
+ - run:
+ name: Code linting (graphql)
+ command: |
+ cd packages/graphql
+ dartfmt **/*.dart -n --set-exit-if-changed
+ - run:
+ name: Code linting (graphql_flutter)
+ command: |
+ cd packages/graphql_flutter
+ dartfmt **/*.dart -n --set-exit-if-changed
+ coverage:
+ docker: *docker_environment
+ steps:
+ - checkout
+ - restore_cache: *restore_cache
+ - run:
+ name: Code coverage (graphql)
+ command: |
+ cd packages/graphql
+ flutter test --coverage
+ - run:
+ name: Code coverage (graphql_flutter)
+ command: |
+ cd packages/graphql_flutter
+ flutter test --coverage
+ - run:
+ name: Upload coverage
+ command: |
+ bash <(curl -s https://codecov.io/bash)
+ release:
+ docker:
+ - image: circleci/node:10
+ steps:
+ - checkout
+ - run:
+ name: Generate release tags
+ command: npx semantic-release@beta
+ publish:
+ docker: *docker_environment
+ steps:
+ - checkout
+ - restore_cache: *restore_cache
+ - run:
+ name: Install yq yaml tool
+ command: |
+ sudo add-apt-repository ppa:rmescandon/yq && \
+ sudo apt update && \
+ sudo apt install yq -y
+ - run:
+ name: Add git credentials
+ command: |
+ git config credential.helper 'cache --timeout=120'
+ git config user.email "github-bot@zino-app.com"
+ git config user.name "Zino App (Github bot)"
+ - run:
+ name: Add pub credentials
+ command: |
+ [[ ! -d ~/.pub-cache ]] && mkdir -p ~/.pub-cache
+
+ cat \<<EOF > ~/.pub-cache/credentials.json
+ {
+ "accessToken": ${PUB_ACCESS_TOKEN},
+ "refreshToken": ${PUB_REFRESH_TOKEN},
+ "tokenEndpoint": "https://accounts.google.com/o/oauth2/token",
+ "scopes": ["https://www.googleapis.com/auth/userinfo.email", "openid"],
+ "expiration": ${PUB_EXPIRATION},
+ }
+ EOF
+ - run:
+ name: Patch pubspec with version (graphql)
+ command: |
+ cd packages/graphql
+
+ cat \<<EOF > patch.yaml
+ version: ${CIRCLE_TAG}
+ EOF
+
+ yq m pubspec.yaml patch.yaml
+ - run:
+ name: Patch pubspec with version (graphql_flutter)
+ command: |
+ cd packages/graphql_flutter
+
+ cat \<<EOF > patch.yaml
+ version: ${CIRCLE_TAG}
+ dependencies:
+ graphql: ^${CIRCLE_TAG}
+ EOF
+
+ yq m pubspec.yaml patch.yaml
+ - run:
+ name: Bump version numbers
+ command: |
+ git commit --allow-empty -m "build: Bump version numbers (via CircleCI) [skip ci]"
+ # Push quietly to prevent showing the token in log
+ git push -q https://${GH_TOKEN}@github.com/zino-app/graphql-flutter.git
+ - run:
+ name: Pub deployment (graphql)
+ command: |
+ cd packages/graphql
+ pub publish -f
+ - run:
+ name: Pub deployment (graphql_flutter)
+ command: |
+ cd packages/graphql
+ pub publish -f
+workflows:
+ default:
+ jobs:
+ - dependencies:
+ filters:
+ tags:
+ only: /^.*$/
+ - lint:
+ requires:
+ - dependencies
+ filters:
+ tags:
+ only: /^.*$/
+ - coverage:
+ requires:
+ - dependencies
+ filters:
+ tags:
+ only: /^.*$/
+ - release:
+ requires:
+ - dependencies
+ - lint
+ - coverage
+ filters:
+ branches:
+ only:
+ - master
+ - beta
+ - publish:
+ requires:
+ - dependencies
+ - lint
+ - coverage
+ filters:
+ tags:
+ only: /^.*$/
+ branches:
+ ignore: /.*/

change_type: DELETE

diff:
-container:
- image: cirrusci/flutter:latest
-
-test_task:
- # pub_cache:
- # folder: $HOME/.pub-cache
- # fingerprint_script: cat pubspec.lock
- # populate_script: pub get
- pub_script: flutter packages get # later when we seperate graphql_client and graphql_flutter, we can change this
- format_script: dartfmt **/*.dart -n --set-exit-if-changed
- # analyze_script: dartanalyzer --fatal-infos .
- publishable_script: pub publish --dry-run
- environment:
- CODECOV_TOKEN: ENCRYPTED[fa57a1dda8bca6ad45ef10043f632b68b7824b60c634415f741a847788c1a44ae9374155e3af3346fb9b6d72bac0e1d3]
- test_script:
- # - pub global activate coverage
- # - dart --enable-vm-service=8111 --pause-isolates-on-exit test/* &
- # - pub global run coverage:collect_coverage --port=8111 --out=coverage.json --wait-paused --resume-isolates
- # - pub global run coverage:format_coverage --lcov --in=coverage.json --out=lcov.info --packages=.packages --report-on=lib
- - flutter test --coverage
- - bash <(curl -s https://codecov.io/bash)

diff:
+branches:
+ - master
+ - name: beta
+ prerelease: true
+ channel: beta
+plugins:
+ - '@semantic-release/commit-analyzer'
+ - '@semantic-release/release-notes-generator'
+ - '@semantic-release/github'

diff:
-Monorepo for [`graphql/client.dart`](./packages/graphql) and [`graphql_flutter`](./packages/graphql_flutter)
-
[![MIT License][license-badge]][license-link]
[![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg)](#contributors)
[![PRs Welcome][prs-badge]](http://makeapullrequest.com)
[![Watch on GitHub](https://img.shields.io/github/watchers/zino-app/graphql-flutter.svg?style=flat&logo=github&colorB=deeppink&label=Watchers)](https://github.com/felangel/bloc)
[![Star on GitHub](https://img.shields.io/github/stars/zino-app/graphql-flutter.svg?style=flat&logo=github&colorB=deeppink&label=Stars)](https://github.com/felangel/bloc)
-
-#### [![Discord](https://img.shields.io/discord/559455668810153989.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/tXTtBfC) Discord channel
-
-
+[![Discord](https://img.shields.io/discord/559455668810153989.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/tXTtBfC)
## GraphQL Flutter
[![Build Status][build-status-badge]][build-status-link]
[![Coverage][coverage-badge]][coverage-link]
-[![version][version-badge]][package-link]
+
+Monorepo for [`graphql/client.dart`](./packages/graphql) and [`graphql_flutter`](./packages/graphql_flutter)
## Roadmap
@@ -55,10 +51,10 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind are welcome!
-[build-status-badge]: https://api.cirrus-ci.com/github/truongsinh/graphql-flutter.svg
-[build-status-link]: https://cirrus-ci.com/github/truongsinh/dart-uuid/master
-[coverage-badge]: https://codecov.io/gh/truongsinh/graphql-flutter/branch/master/graph/badge.svg
-[coverage-link]: https://codecov.io/gh/truongsinh/graphql-flutter
+[build-status-badge]: https://img.shields.io/circleci/project/github/zino-app/graphql-flutter/master.svg
+[build-status-link]: https://circleci.com/gh/zino-app/graphql-flutter
+[coverage-badge]: https://codecov.io/gh/zino-app/graphql-flutter/branch/master/graph/badge.svg
+[coverage-link]: https://codecov.io/gh/zino-app/graphql-flutter
[version-badge]: https://img.shields.io/pub/v/graphql_flutter.svg
[package-link]: https://pub.dartlang.org/packages/graphql_flutter/versions/1.0.0-alpha.3
[license-badge]: https://img.shields.io/github/license/zino-app/graphql-flutter.svg

diff:
[![Watch on GitHub](https://img.shields.io/github/watchers/zino-app/graphql-flutter.svg?style=flat&logo=github&colorB=deeppink&label=Watchers)](https://github.com/felangel/bloc)
[![Star on GitHub](https://img.shields.io/github/stars/zino-app/graphql-flutter.svg?style=flat&logo=github&colorB=deeppink&label=Stars)](https://github.com/felangel/bloc)
-
-#### [![Discord](https://img.shields.io/discord/559455668810153989.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/tXTtBfC) Discord channel
-
+[![Discord](https://img.shields.io/discord/559455668810153989.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/tXTtBfC)
## Usage
```dart
import 'package:graphql/client.dart';
```
-Usage for the standalone client is still poorly documented
+Usage for the standalone client is still poorly documented
## Contributing

diff:
@@ -9,7 +9,7 @@ authors:
homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter
dependencies:
- graphql: ^1.0.1-beta # path: ../graphql
+ graphql: ^1.0.1-beta
flutter:
sdk: flutter
meta: ^1.1.6
@@ -25,4 +25,4 @@ dev_dependencies:
test: ^1.5.3
environment:
- sdk: ">=2.2.0 <3.0.0"
+ sdk: ">=2.1.0 <3.0.0"

original_message: chore: implement ci and cd