Skip to content

Commit

Permalink
Merge pull request #435 from mainawycliffe/handle-errors
Browse files Browse the repository at this point in the history
Move Mutation Callbacks to MutationOptions
  • Loading branch information
micimize committed Nov 5, 2019
2 parents 602aa7f + 66b13f5 commit cbfd977
Show file tree
Hide file tree
Showing 16 changed files with 242 additions and 139 deletions.
2 changes: 1 addition & 1 deletion packages/graphql/lib/src/cache/in_memory_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class InMemoryCache implements Cache {

Future<HashMap<String, dynamic>> _readFromStorage() async {
try {
final decoded = jsonDecode(window.localStorage[masterKey]);
final decoded = jsonDecode(window.localStorage[masterKey]);
return HashMap.from(decoded);
} catch (error) {
// TODO: handle error
Expand Down
4 changes: 1 addition & 3 deletions packages/graphql/lib/src/cache/in_memory_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ 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;
Expand Down
7 changes: 4 additions & 3 deletions packages/graphql/lib/src/cache/in_memory_stub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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);

Expand All @@ -16,10 +17,10 @@ class InMemoryCache implements Cache {
///
/// For vm/flutter, `storagePrefix` is a path to the directory
/// that can save `cache.txt` file
///
/// For flutter usually provided by
///
/// For flutter usually provided by
/// [path_provider.getApplicationDocumentsDirectory]
///
///
/// @NotNull
final FutureOr<String> storagePrefix;

Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/lib/src/cache/normalized_in_memory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class NormalizedInMemoryCache extends InMemoryCache {
}

@override
void reset(){
void reset() {
data.clear();
}

Expand Down
19 changes: 18 additions & 1 deletion packages/graphql/lib/src/core/query_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,24 @@ class QueryManager {
}

Future<QueryResult> mutate(MutationOptions options) {
return fetchQuery('0', options);
return fetchQuery('0', options).then((result) async {
// not sure why query id is '0', may be needs improvements
// once the mutation has been process successfully, execute callbacks
// before returning the results
final mutationCallbacks = MutationCallbacks(
cache: cache,
options: options,
queryId: '0',
);

final callbacks = mutationCallbacks.callbacks;

for (final callback in callbacks) {
await callback(result);
}

return result;
});
}

Future<QueryResult> fetchQuery(
Expand Down
121 changes: 117 additions & 4 deletions packages/graphql/lib/src/core/query_options.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'package:meta/meta.dart';

import 'package:gql/ast.dart';
import 'package:gql/language.dart';
import 'package:meta/meta.dart';

import 'package:graphql/client.dart';
import 'package:graphql/internal.dart';
import 'package:graphql/src/utilities/helpers.dart';
import 'package:graphql/src/core/raw_operation_data.dart';

Expand Down Expand Up @@ -123,6 +126,10 @@ class QueryOptions extends BaseOptions {
int pollInterval;
}

typedef OnMutationCompleted = void Function(dynamic data);
typedef OnMutationUpdate = void Function(Cache cache, QueryResult result);
typedef OnError = void Function(OperationException error);

/// Mutation options
class MutationOptions extends BaseOptions {
MutationOptions({
Expand All @@ -133,13 +140,107 @@ class MutationOptions extends BaseOptions {
FetchPolicy fetchPolicy,
ErrorPolicy errorPolicy,
Map<String, dynamic> context,
this.onCompleted,
this.update,
this.onError,
}) : super(
policies: Policies(fetch: fetchPolicy, error: errorPolicy),
document: document,
documentNode: documentNode,
variables: variables,
context: context,
);

OnMutationCompleted onCompleted;
OnMutationUpdate update;
OnError onError;
}

class MutationCallbacks {
final MutationOptions options;
final Cache cache;
final String queryId;

MutationCallbacks({
this.options,
this.cache,
this.queryId,
}) : assert(cache != null),
assert(options != null),
assert(queryId != null);

// callbacks will be called against each result in the stream,
// which should then rebroadcast queries with the appropriate optimism
Iterable<OnData> get callbacks =>
<OnData>[onCompleted, update, onError].where(notNull);

// Todo: probably move this to its own class
OnData get onCompleted {
if (options.onCompleted != null) {
return (QueryResult result) {
if (!result.loading && !result.optimistic) {
return options.onCompleted(result.data);
}
};
}
return null;
}

OnData get onError {
if (options.onError != null) {
return (QueryResult result) {
if (!result.loading &&
result.hasException &&
options.errorPolicy != ErrorPolicy.ignore) {
return options.onError(result.exception);
}
};
}

return null;
}

/// The optimistic cache layer id `update` will write to
/// is a "child patch" of the default optimistic patch
/// created by the query manager
String get _patchId => '${queryId}.update';

/// apply the user's patch
void _optimisticUpdate(QueryResult result) {
final String patchId = _patchId;
// this is also done in query_manager, but better safe than sorry
assert(cache is OptimisticCache,
"can't optimisticly update non-optimistic cache");
(cache as OptimisticCache).addOptimisiticPatch(patchId, (Cache cache) {
options.update(cache, result);
return cache;
});
}

// optimistic patches will be cleaned up by the query_manager
// cleanup is handled by heirarchical optimism -
// as in, because our patch id is prefixed with '${observableQuery.queryId}.',
// it will be discarded along with the observableQuery.queryId patch
// TODO this results in an implicit coupling with the patch id system
OnData get update {
if (options.update != null) {
// dereference all variables that might be needed if the widget is disposed
final OnMutationUpdate widgetUpdate = options.update;
final OnData optimisticUpdate = _optimisticUpdate;

// wrap update logic to handle optimism
void updateOnData(QueryResult result) {
if (result.optimistic) {
return optimisticUpdate(result);
} else {
return widgetUpdate(cache, result);
}
}

return updateOnData;
}
return null;
}
}

// ObservableQuery options
Expand Down Expand Up @@ -218,10 +319,13 @@ class FetchMoreOptions {
DocumentNode documentNode,
this.variables = const <String, dynamic>{},
@required this.updateQuery,
}) : assert((document != null && documentNode == null) ||
(document == null && documentNode != null)),
}) : assert(
_mutuallyExclusive(document, documentNode),
'"document" or "documentNode" options are mutually exclusive.',
),
assert(updateQuery != null),
documentNode = parseString(document);
this.documentNode =
documentNode ?? document != null ? parseString(document) : null;

DocumentNode documentNode;

Expand All @@ -242,3 +346,12 @@ class FetchMoreOptions {
/// with the result data already in the cache
UpdateQuery updateQuery;
}

bool _mutuallyExclusive(
Object a,
Object b, {
bool required = false,
}) =>
(!required && a == null && b == null) ||
(a != null && b == null) ||
(a == null && b != null);
7 changes: 6 additions & 1 deletion packages/graphql/lib/src/core/raw_operation_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ class RawOperationData {
Map<String, dynamic> variables,
String operationName,
}) : assert(
document != null || documentNode != null,
'Either a "document" or "documentNode" option is required. '
'You must specify your GraphQL document in the query options.',
),
assert(
(document != null && documentNode == null) ||
(document == null && documentNode != null),
'"document" or "documentNode" option is required. You must specify your GraphQL document in the query options.',
'"document" or "documentNode" options are mutually exclusive.',
),
documentNode = documentNode ?? parseString(document),
_operationName = operationName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Future<Map<String, MultipartFile>> deprecatedHelper(

bool isIoFile(object) {
final r = object is io.File;
if(r) {
if (r) {
print(r'''
⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️ DEPRECATION WARNING ⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️⚠️️️️️️️️
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import 'package:http/http.dart';
// in case the body is io.File
// in future release, io.File will no longer be supported
// but this stub is noop
Future<Map<String, MultipartFile>> deprecatedHelper(body, currentMap, currentPath) async => null;
Future<Map<String, MultipartFile>> deprecatedHelper(
body, currentMap, currentPath) async =>
null;

// @deprecated, backward compatible only
// in case the body is io.File
Expand Down
20 changes: 10 additions & 10 deletions packages/graphql/lib/src/utilities/file_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';

Stream<List<int>> _readFile(html.File file) {
final reader = html.FileReader();
final streamController = StreamController<List<int>>();
final reader = html.FileReader();
final streamController = StreamController<List<int>>();

reader.onLoad.listen((_) {
// streamController.add(reader.result);
streamController.close();
});
reader.onLoad.listen((_) {
// streamController.add(reader.result);
streamController.close();
});

reader.onError.listen((error) => streamController.addError(error));
reader.onError.listen((error) => streamController.addError(error));

reader.readAsArrayBuffer(file);
reader.readAsArrayBuffer(file);

return streamController.stream;
}
return streamController.stream;
}

MultipartFile multipartFileFrom(html.File f) => MultipartFile(
'',
Expand Down
3 changes: 2 additions & 1 deletion packages/graphql/lib/src/utilities/file_stub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import 'dart:async';

import 'package:http/http.dart';

FutureOr<MultipartFile> multipartFileFrom(/*io.File or html.File*/ f) => throw UnsupportedError('io or html');
FutureOr<MultipartFile> multipartFileFrom(/*io.File or html.File*/ f) =>
throw UnsupportedError('io or html');
10 changes: 5 additions & 5 deletions packages/graphql/test/link/http/link_http_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ void main() {
);
expect(
captured.body,
'{"operationName":"Operation","variables":{},"query":"query Operation {}"}',
'{"operationName":"Operation","variables":{},"query":"query Operation {\\n \\n}"}',
);
});

Expand Down Expand Up @@ -163,7 +163,7 @@ void main() {
);
expect(
captured.body,
'{"operationName":"Operation","variables":{},"extensions":null,"query":"query Operation {}"}',
'{"operationName":"Operation","variables":{},"extensions":null,"query":"query Operation {\\n \\n}"}',
);
});

Expand Down Expand Up @@ -215,7 +215,7 @@ void main() {
);
expect(
captured.body,
'{"operationName":"Operation","variables":{},"extensions":null,"query":"query Operation {}"}',
'{"operationName":"Operation","variables":{},"extensions":null,"query":"query Operation {\\n \\n}"}',
);
});

Expand Down Expand Up @@ -252,7 +252,7 @@ void main() {

expect(
captured.body,
'{"operationName":null,"variables":{},"extensions":{"extension-1":"extension-value-1"},"query":"{}"}',
'{"operationName":null,"variables":{},"extensions":{"extension-1":"extension-value-1"},"query":"query {\\n \\n}"}',
);
});

Expand Down Expand Up @@ -519,7 +519,7 @@ void main() {
r'''--dart-http-boundary-REPLACED
content-disposition: form-data; name="operations"
{"operationName":null,"variables":{"files":[null,null]},"query":"{}"}
{"operationName":null,"variables":{"files":[null,null]},"query":"query {\n \n}"}
--dart-http-boundary-REPLACED
content-disposition: form-data; name="map"
Expand Down
8 changes: 8 additions & 0 deletions packages/graphql_flutter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,14 @@ The syntax for mutations is fairly similar to that of a query. The only differen
Mutation(
options: MutationOptions(
document: addStar, // this is the mutation string you just created
// you can update the cache based on results
update: (Cache cache, QueryResult result) {
return cache;
},
// or do something with the result.data on completion
onCompleted: (dynamic resultData) {
print(resultData);
},
),
builder: (
RunMutation runMutation,
Expand Down
Loading

0 comments on commit cbfd977

Please sign in to comment.