message:
feat: Use hooks
Re-implement widgets using flutter_hooks and expose said hooks.

diff:
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

diff:
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

diff:
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end

diff:
@@ -10,4 +10,9 @@ export 'package:graphql_flutter/src/widgets/query.dart';
export 'package:graphql_flutter/src/widgets/subscription.dart';
export 'package:graphql_flutter/src/widgets/result_accumulator.dart';
+export 'package:graphql_flutter/src/widgets/hooks/mutation.dart';
+export 'package:graphql_flutter/src/widgets/hooks/query.dart';
+export 'package:graphql_flutter/src/widgets/hooks/subscription.dart';
+export 'package:graphql_flutter/src/widgets/hooks/watch_query.dart';
+
export 'package:graphql_flutter/src/hive_init.dart';

diff:
import 'package:flutter/widgets.dart';
import 'package:graphql/client.dart';
+export 'package:graphql_flutter/src/widgets/hooks/query.dart';
class GraphQLProvider extends StatefulWidget {
const GraphQLProvider({

diff:
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:graphql_flutter/graphql_flutter.dart';
+
+GraphQLClient useGraphQLClient() {
+ final context = useContext();
+ return useValueListenable(GraphQLProvider.of(context));
+}

diff:
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
+import 'package:graphql_flutter/src/widgets/query.dart';
+
+typedef RunMutation<TParsed> = MultiSourceResult<TParsed> Function(
+ Map<String, dynamic> variables, {
+ Object? optimisticResult,
+});
+
+class MutationHookResult<TParsed> {
+ final RunMutation<TParsed> runMutation;
+ final QueryResult<TParsed> result;
+
+ MutationHookResult({
+ required this.runMutation,
+ required this.result,
+ });
+}
+
+MutationHookResult<TParsed> useMutation<TParsed>(
+ MutationOptions<TParsed> options,
+) {
+ final watchOptions = useMemoized(
+ () => options.asWatchQueryOptions(),
+ [options],
+ );
+ final client = useGraphQLClient();
+ final query = useWatchMutation<TParsed>(watchOptions);
+ final snapshot = useStream(
+ query.stream,
+ initialData: query.latestResult ?? QueryResult.unexecuted,
+ );
+ final runMutation = useCallback((
+ Map<String, dynamic> variables, {
+ Object? optimisticResult,
+ }) {
+ final mutationCallbacks = MutationCallbackHandler(
+ cache: client.cache,
+ queryId: query.queryId,
+ options: options,
+ );
+ return (query
+ ..variables = variables
+ ..optimisticResult = optimisticResult
+ ..onData(mutationCallbacks.callbacks) // add callbacks to observable
+ )
+ .fetchResults();
+ }, [client, query, options]);
+ return MutationHookResult(
+ runMutation: runMutation,
+ result: snapshot.data!,
+ );
+}

diff:
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql_flutter/src/widgets/hooks/watch_query.dart';
+
+// method to call from widget to fetchmore queries
+typedef FetchMore<TParsed> = Future<QueryResult<TParsed>> Function(
+ FetchMoreOptions options);
+
+typedef Refetch<TParsed> = Future<QueryResult<TParsed>?> Function();
+
+class QueryHookResult<TParsed> {
+ final QueryResult<TParsed> result;
+ final Refetch<TParsed> refetch;
+ final FetchMore<TParsed> fetchMore;
+
+ QueryHookResult({
+ required this.result,
+ required this.refetch,
+ required this.fetchMore,
+ });
+}
+
+QueryHookResult<TParsed> useQuery<TParsed>(QueryOptions<TParsed> options) {
+ final watchQueryOptions = useMemoized(
+ () => options.asWatchQueryOptions(),
+ [options],
+ );
+ final query = useWatchQuery(watchQueryOptions);
+ final snapshot = useStream(
+ query.stream,
+ initialData: query.latestResult,
+ );
+ return QueryHookResult(
+ result: snapshot.data!,
+ refetch: query.refetch,
+ fetchMore: query.fetchMore,
+ );
+}

diff:
+import 'dart:async';
+import 'dart:io';
+
+import 'package:connectivity_plus/connectivity_plus.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
+
+typedef OnSubscriptionResult<TParsed> = void Function(
+ QueryResult<TParsed> subscriptionResult,
+ GraphQLClient? client,
+);
+
+typedef SubscriptionBuilder<TParsed> = Widget Function(
+ QueryResult<TParsed> result);
+
+QueryResult<TParsed> useSubscription<TParsed>(
+ SubscriptionOptions<TParsed> options, {
+ OnSubscriptionResult<TParsed>? onSubscriptionResult,
+}) {
+ final client = useGraphQLClient();
+ final stream = use(_SubscriptionHook(
+ client: client,
+ onSubscriptionResult: onSubscriptionResult,
+ options: options,
+ ));
+ final snapshot = useStream(
+ stream,
+ initialData: options.optimisticResult != null
+ ? QueryResult.optimistic(
+ data: options.optimisticResult as Map<String, dynamic>?,
+ parserFn: options.parserFn,
+ )
+ : QueryResult.loading(parserFn: options.parserFn),
+ );
+ return snapshot.data!;
+}
+
+class _SubscriptionHook<TParsed> extends Hook<Stream<QueryResult<TParsed>>> {
+ final SubscriptionOptions<TParsed> options;
+ final GraphQLClient client;
+ final OnSubscriptionResult<TParsed>? onSubscriptionResult;
+ _SubscriptionHook({
+ required this.options,
+ required this.client,
+ required this.onSubscriptionResult,
+ });
+ @override
+ HookState<Stream<QueryResult<TParsed>>, Hook<Stream<QueryResult<TParsed>>>>
+ createState() {
+ return _SubscriptionHookState();
+ }
+}
+
+class _SubscriptionHookState<TParsed> extends HookState<
+ Stream<QueryResult<TParsed>>, _SubscriptionHook<TParsed>> {
+ late Stream<QueryResult<TParsed>> stream;
+ GraphQLClient? client;
+
+ ConnectivityResult? _currentConnectivityResult;
+ StreamSubscription<ConnectivityResult>? _networkSubscription;
+
+ void _initSubscription() {
+ stream = client!.subscribe(hook.options);
+ final onSubscriptionResult = hook.onSubscriptionResult;
+ if (onSubscriptionResult != null) {
+ stream = stream.map((result) {
+ onSubscriptionResult(result, client);
+ return result;
+ });
+ }
+ }
+
+ @override
+ void initHook() {
+ super.initHook();
+ _networkSubscription =
+ Connectivity().onConnectivityChanged.listen(_onNetworkChange);
+ }
+
+ @override
+ void didUpdateHook(_SubscriptionHook<TParsed> oldHook) {
+ super.didUpdateHook(oldHook);
+
+ if (hook.options != oldHook.options || hook.client != oldHook.client) {
+ _initSubscription();
+ }
+ }
+
+ @override
+ void dispose() {
+ _networkSubscription?.cancel();
+ super.dispose();
+ }
+
+ Future _onNetworkChange(ConnectivityResult result) async {
+ //if from offline to online
+ if (_currentConnectivityResult == ConnectivityResult.none &&
+ (result == ConnectivityResult.mobile ||
+ result == ConnectivityResult.wifi)) {
+ _currentConnectivityResult = result;
+
+ // android connectivitystate cannot be trusted
+ // validate with nslookup
+ if (Platform.isAndroid) {
+ try {
+ final nsLookupResult = await InternetAddress.lookup('google.com');
+ if (nsLookupResult.isNotEmpty &&
+ nsLookupResult[0].rawAddress.isNotEmpty) {
+ _initSubscription();
+ }
+ // on exception -> no real connection, set current state to none
+ } on SocketException catch (_) {
+ _currentConnectivityResult = ConnectivityResult.none;
+ }
+ } else {
+ _initSubscription();
+ }
+ } else {
+ _currentConnectivityResult = result;
+ }
+ }
+
+ @override
+ Stream<QueryResult<TParsed>> build(BuildContext context) {
+ return stream;
+ }
+}

diff:
+import 'package:flutter/widgets.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
+
+class _WatchQueryHook<TParsed> extends Hook<ObservableQuery<TParsed>> {
+ final GraphQLClient client;
+ final WatchQueryOptions<TParsed> options;
+
+ _WatchQueryHook({
+ required this.options,
+ required this.client,
+ });
+
+ @override
+ HookState<ObservableQuery<TParsed>, Hook<ObservableQuery<TParsed>>>
+ createState() {
+ return _WatchQueryHookState();
+ }
+}
+
+class _WatchQueryHookState<TParsed>
+ extends HookState<ObservableQuery<TParsed>, _WatchQueryHook<TParsed>> {
+ late ObservableQuery<TParsed> _observableQuery;
+
+ @override
+ initHook() {
+ super.initHook();
+ _connect();
+ }
+
+ @override
+ dispose() {
+ _close();
+ super.dispose();
+ }
+
+ _connect() {
+ _observableQuery = hook.client.queryManager.watchQuery(hook.options);
+ }
+
+ _close() {
+ _observableQuery.close();
+ }
+
+ _reconnect() {
+ _close();
+ _connect();
+ }
+
+ @override
+ didUpdateHook(oldHook) {
+ super.didUpdateHook(oldHook);
+ if (oldHook.client == hook.client && oldHook.options == hook.options) {
+ return;
+ }
+ _reconnect();
+ }
+
+ ObservableQuery<TParsed> build(BuildContext context) {
+ return _observableQuery;
+ }
+}
+
+ObservableQuery<TParsed> useWatchQuery<TParsed>(
+ WatchQueryOptions<TParsed> options,
+) {
+ final client = useGraphQLClient();
+ final overwrittenOptions = useMemoized(() {
+ final policies =
+ client.defaultPolicies.query.withOverrides(options.policies);
+ return options.copyWithPolicies(policies);
+ }, [options]);
+
+ return use(_WatchQueryHook(
+ options: overwrittenOptions,
+ client: client,
+ ));
+}
+
+ObservableQuery<TParsed> useWatchMutation<TParsed>(
+ WatchQueryOptions<TParsed> options) {
+ final client = useGraphQLClient();
+ final overwrittenOptions = useMemoized(() {
+ final policies =
+ client.defaultPolicies.mutate.withOverrides(options.policies);
+ return options.copyWithPolicies(policies);
+ }, [options]);
+ return use(
+ _WatchQueryHook(
+ options: overwrittenOptions,
+ client: client,
+ ),
+ );
+}

diff:
import 'package:flutter/widgets.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql/client.dart';
+import 'package:graphql_flutter/src/widgets/hooks/mutation.dart';
-import 'package:graphql_flutter/src/widgets/graphql_provider.dart';
-
-typedef RunMutation<TParsed> = MultiSourceResult<TParsed> Function(
- Map<String, dynamic> variables, {
- Object? optimisticResult,
-});
+export 'package:graphql_flutter/src/widgets/hooks/mutation.dart';
typedef MutationBuilder<TParsed> = Widget Function(
RunMutation<TParsed> runMutation,
@@ -16,7 +13,7 @@ typedef MutationBuilder<TParsed> = Widget Function(
/// Builds a [Mutation] widget based on the a given set of [MutationOptions]
/// that streams [QueryResult]s into the [QueryBuilder].
-class Mutation<TParsed> extends StatefulWidget {
+class Mutation<TParsed> extends HookWidget {
const Mutation({
final Key? key,
required this.options,
@@ -26,109 +23,9 @@ class Mutation<TParsed> extends StatefulWidget {
final MutationOptions<TParsed> options;
final MutationBuilder<TParsed> builder;
- @override
- MutationState<TParsed> createState() => MutationState();
-}
-
-class MutationState<TParsed> extends State<Mutation<TParsed>> {
- GraphQLClient? client;
- ObservableQuery<TParsed>? observableQuery;
-
- WatchQueryOptions<TParsed>? __cachedOptions;
-
- WatchQueryOptions<TParsed> get _providedOptions {
- final _options = WatchQueryOptions<TParsed>(
- document: widget.options.document,
- operationName: widget.options.operationName,
- variables: widget.options.variables,
- fetchPolicy: widget.options.fetchPolicy,
- errorPolicy: widget.options.errorPolicy,
- cacheRereadPolicy: widget.options.cacheRereadPolicy,
- fetchResults: false,
- context: widget.options.context,
- parserFn: widget.options.parserFn,
- );
- __cachedOptions ??= _options;
- return _options;
- }
-
- /// sets new options, returning true if they didn't equal the old
- bool _setNewOptions() {
- final _cached = __cachedOptions;
- final _new = _providedOptions;
- if (_cached == null || !_new.equal(_cached)) {
- __cachedOptions = _new;
- return true;
- }
- return false;
- }
-
- // TODO is it possible to extract shared logic into mixin
- void _initQuery() {
- observableQuery?.close();
- observableQuery = client!.watchMutation(_providedOptions.copy());
- }
-
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- final GraphQLClient client = GraphQLProvider.of(context).value;
- if (client != this.client) {
- this.client = client;
- _initQuery();
- }
- }
-
- @override
- void didUpdateWidget(Mutation<TParsed> oldWidget) {
- super.didUpdateWidget(oldWidget);
-
- // TODO @micimize - investigate why/if this was causing issues
- if (_setNewOptions()) {
- _initQuery();
- }
- }
-
- /// Run the mutation with the given `variables` and `optimisticResult`,
- /// returning a [MultiSourceResult] for handling both the eager and network results
- MultiSourceResult<TParsed> runMutation(
- Map<String, dynamic> variables, {
- Object? optimisticResult,
- }) {
- final mutationCallbacks = MutationCallbackHandler(
- cache: client!.cache,
- queryId: observableQuery!.queryId,
- options: widget.options,
- );
-
- return (observableQuery!
- ..variables = variables
- ..options.optimisticResult = optimisticResult
- ..onData(mutationCallbacks.callbacks) // add callbacks to observable
- )
- .fetchResults();
- }
-
- @override
- void dispose() {
- observableQuery?.close(force: false);
- super.dispose();
- }
-
@override
Widget build(BuildContext context) {
- return StreamBuilder<QueryResult<TParsed>?>(
- initialData: observableQuery?.latestResult ?? QueryResult.unexecuted,
- stream: observableQuery?.stream,
- builder: (
- BuildContext buildContext,
- AsyncSnapshot<QueryResult<TParsed>?> snapshot,
- ) {
- return widget.builder(
- runMutation,
- snapshot.data,
- );
- },
- );
+ final result = useMutation(options);
+ return builder(result.runMutation, result.result);
}
}

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

diff:
-import 'dart:async';
-import 'dart:io';
-
-import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/widgets.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
-typedef OnSubscriptionResult<TParsed> = void Function(
- QueryResult<TParsed> subscriptionResult,
- GraphQLClient? client,
-);
-
-typedef SubscriptionBuilder<TParsed> = Widget Function(
- QueryResult<TParsed> result);
-
/// Creats a subscription with [GraphQLClient.subscribe].
///
/// The [builder] is passed a [QueryResult] with only the **most recent**
@@ -63,7 +52,7 @@ typedef SubscriptionBuilder<TParsed> = Widget Function(
/// }
/// ```
/// {@end-tool}
-class Subscription<TParsed> extends StatefulWidget {
+class Subscription<TParsed> extends HookWidget {
const Subscription({
required this.options,
required this.builder,
@@ -75,105 +64,12 @@ class Subscription<TParsed> extends StatefulWidget {
final SubscriptionBuilder<TParsed> builder;
final OnSubscriptionResult<TParsed>? onSubscriptionResult;
- @override
- _SubscriptionState<TParsed> createState() => _SubscriptionState();
-}
-
-class _SubscriptionState<TParsed> extends State<Subscription<TParsed>> {
- Stream<QueryResult<TParsed>>? stream;
- GraphQLClient? client;
-
- ConnectivityResult? _currentConnectivityResult;
- StreamSubscription<ConnectivityResult>? _networkSubscription;
-
- void _initSubscription() {
- stream = client!.subscribe(widget.options);
-
- if (widget.onSubscriptionResult != null) {
- stream = stream!.map((result) {
- widget.onSubscriptionResult!(result, client);
- return result;
- });
- }
- }
-
- @override
- void initState() {
- _networkSubscription =
- Connectivity().onConnectivityChanged.listen(_onNetworkChange);
-
- super.initState();
- }
-
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- final GraphQLClient newClient = GraphQLProvider.of(context).value;
- if (client != newClient) {
- client = newClient;
- _initSubscription();
- }
- }
-
- @override
- void didUpdateWidget(Subscription<TParsed> oldWidget) {
- super.didUpdateWidget(oldWidget);
-
- if (!widget.options.equal(oldWidget.options)) {
- _initSubscription();
- }
- }
-
- @override
- void dispose() {
- _networkSubscription?.cancel();
- super.dispose();
- }
-
- Future _onNetworkChange(ConnectivityResult result) async {
- //if from offline to online
- if (_currentConnectivityResult == ConnectivityResult.none &&
- (result == ConnectivityResult.mobile ||
- result == ConnectivityResult.wifi)) {
- _currentConnectivityResult = result;
-
- // android connectivitystate cannot be trusted
- // validate with nslookup
- if (Platform.isAndroid) {
- try {
- final nsLookupResult = await InternetAddress.lookup('google.com');
- if (nsLookupResult.isNotEmpty &&
- nsLookupResult[0].rawAddress.isNotEmpty) {
- _initSubscription();
- }
- // on exception -> no real connection, set current state to none
- } on SocketException catch (_) {
- _currentConnectivityResult = ConnectivityResult.none;
- }
- } else {
- _initSubscription();
- }
- } else {
- _currentConnectivityResult = result;
- }
- }
-
@override
Widget build(BuildContext context) {
- return StreamBuilder<QueryResult<TParsed>>(
- initialData: widget.options.optimisticResult != null
- ? QueryResult.optimistic(
- data: widget.options.optimisticResult as Map<String, dynamic>?,
- parserFn: widget.options.parserFn,
- )
- : QueryResult.loading(parserFn: widget.options.parserFn),
- stream: stream,
- builder: (
- BuildContext buildContext,
- AsyncSnapshot<QueryResult<TParsed>> snapshot,
- ) {
- return widget.builder(snapshot.data!);
- },
+ final result = useSubscription<TParsed>(
+ options,
+ onSubscriptionResult: onSubscriptionResult,
);
+ return builder(result);
}
}

diff:
@@ -13,6 +13,7 @@ dependencies:
connectivity_plus: ^2.0.3
hive: ^2.0.0
plugin_platform_interface: ^2.0.0
+ flutter_hooks: ^0.18.2
dev_dependencies:
pedantic: ^1.11.0
mockito: ^5.0.0

diff:
@@ -32,17 +32,19 @@ final query = gql("""
Future<void> mockApplicationDocumentsDirectory() async {
// Create a temporary directory.
final directory = await Directory.systemTemp.createTemp();
-
- // Mock out the MethodChannel for the path_provider plugin.
- const MethodChannel('plugins.flutter.io/path_provider')
- .setMockMethodCallHandler((MethodCall methodCall) async {
+ final handler = (MethodCall methodCall) async {
// If you're getting the apps documents directory, return the path to the
// temp directory on the test environment instead.
if (methodCall.method == 'getApplicationDocumentsDirectory') {
return directory.path;
}
return null;
- });
+ };
+ // Mock out the MethodChannel for the path_provider plugin.
+ const MethodChannel('plugins.flutter.io/path_provider')
+ .setMockMethodCallHandler(handler);
+ const MethodChannel('plugins.flutter.io/path_provider_macos')
+ .setMockMethodCallHandler(handler);
}
class Page extends StatefulWidget {
@@ -339,7 +341,7 @@ void main() {
'does not issues new network request when policies are effectively unchanged',
(WidgetTester tester) async {
final page = Page(
- fetchPolicy: FetchPolicy.cacheAndNetwork,
+ fetchPolicy: client!.value.defaultPolicies.query.fetch,
errorPolicy: null,
);


message: feat: Clean-up imports

change_type: MODIFY

diff:
import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql/client.dart';
+import 'package:graphql_flutter/src/widgets/graphql_provider.dart';
GraphQLClient useGraphQLClient() {
final context = useContext();

diff:
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:graphql/client.dart';
import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
-import 'package:graphql_flutter/src/widgets/query.dart';
typedef RunMutation<TParsed> = MultiSourceResult<TParsed> Function(
Map<String, dynamic> variables, {

diff:
import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql/client.dart';
+import 'package:graphql_flutter/src/widgets/hooks/watch_query.dart';
// method to call from widget to fetchmore queries
typedef FetchMore<TParsed> = Future<QueryResult<TParsed>> Function(

diff:
@@ -4,7 +4,7 @@ import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql/client.dart';
import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
typedef OnSubscriptionResult<TParsed> = void Function(

diff:
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql/client.dart';
import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
class _WatchQueryHook<TParsed> extends Hook<ObservableQuery<TParsed>> {

diff:
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-
import 'package:graphql/client.dart';
import 'package:graphql_flutter/src/widgets/hooks/mutation.dart';
-export 'package:graphql_flutter/src/widgets/hooks/mutation.dart';
-
typedef MutationBuilder<TParsed> = Widget Function(
RunMutation<TParsed> runMutation,
QueryResult<TParsed>? result,

diff:
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-
-import 'package:graphql_flutter/graphql_flutter.dart';
-export 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql/client.dart';
+import 'package:graphql_flutter/src/widgets/hooks/query.dart';
typedef QueryBuilder<TParsed> = Widget Function(
QueryResult<TParsed> result, {

diff:
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql/client.dart';
+import 'package:graphql_flutter/src/widgets/hooks/subscription.dart';
/// Creats a subscription with [GraphQLClient.subscribe].
///

license: MIT License

repo: zino-hofmann/graphql-flutter

original_message: feat: Clean-up imports


message: fix: Cache-reboardcast error

diff:
import 'dart:async';
import 'package:graphql/client.dart';
+import 'package:graphql/src/utilities/response.dart';
import 'package:meta/meta.dart';
import 'package:graphql/src/core/fetch_more.dart';
@@ -231,6 +232,17 @@ class ObservableQuery<TParsed> {
);
}
+ void addFetchResult(
+ Response response,
+ QueryResultSource source, {
+ bool fromRebroadcast = false,
+ }) {
+ addResult(
+ mapFetchResultToQueryResult<TParsed>(response, options, source: source),
+ fromRebroadcast: fromRebroadcast,
+ );
+ }
+
/// Add a [result] to the [stream] unless it was created
/// before [lasestResult].
///

diff:
import 'dart:async';
import 'package:graphql/src/core/result_parser.dart';
+import 'package:graphql/src/utilities/response.dart';
import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
@@ -458,19 +459,16 @@ class QueryManager {
return false;
}
- for (ObservableQuery query in queries.values) {
+ for (var query in queries.values) {
if (query != exclude && query.isRebroadcastSafe) {
final cachedData = cache.readQuery(
query.options.asRequest,
optimistic: query.options.policies.mergeOptimisticData,
);
if (_cachedDataHasChangedFor(query, cachedData)) {
- query.addResult(
- mapFetchResultToQueryResult(
+ query.addFetchResult(
Response(data: cachedData),
- query.options,
- source: QueryResultSource.cache,
- ),
+ QueryResultSource.cache,
fromRebroadcast: true,
);
}
@@ -504,47 +502,6 @@ class QueryManager {
return requestId;
}
-
- QueryResult<TParsed> mapFetchResultToQueryResult<TParsed>(
- Response response,
- BaseOptions<TParsed> options, {
- required QueryResultSource source,
- }) {
- List<GraphQLError>? errors;
- dynamic data;
-
- // check if there are errors and apply the error policy if so
- // in a nutshell: `ignore` swallows errors, `none` swallows data
- if (response.errors != null && response.errors!.isNotEmpty) {
- switch (options.errorPolicy) {
- case ErrorPolicy.all:
- // handle both errors and data
- errors = response.errors;
- data = response.data;
- break;
- case ErrorPolicy.ignore:
- // ignore errors
- data = response.data;
- break;
- case ErrorPolicy.none:
- default:
- // TODO not actually sure if apollo even casts graphql errors in `none` mode,
- // it's also kind of legacy
- errors = response.errors;
- break;
- }
- } else {
- data = response.data;
- }
-
- return QueryResult(
- data: data,
- context: response.context,
- source: source,
- exception: coalesceErrors(graphqlErrors: errors),
- parserFn: options.parserFn,
- );
- }
}
QueryResult<TParsed> _wrapFailure<TParsed>(

change_type: ADD

diff:
+import 'package:graphql/src/core/_base_options.dart';
+import 'package:graphql/src/core/core.dart';
+import 'package:graphql/src/exceptions.dart';
+
+QueryResult<TParsed> mapFetchResultToQueryResult<TParsed>(
+ Response response,
+ BaseOptions<TParsed> options, {
+ required QueryResultSource source,
+}) {
+ List<GraphQLError>? errors;
+ dynamic data;
+
+ // check if there are errors and apply the error policy if so
+ // in a nutshell: `ignore` swallows errors, `none` swallows data
+ if (response.errors != null && response.errors!.isNotEmpty) {
+ switch (options.errorPolicy) {
+ case ErrorPolicy.all:
+ // handle both errors and data
+ errors = response.errors;
+ data = response.data;
+ break;
+ case ErrorPolicy.ignore:
+ // ignore errors
+ data = response.data;
+ break;
+ case ErrorPolicy.none:
+ default:
+ // TODO not actually sure if apollo even casts graphql errors in `none` mode,
+ // it's also kind of legacy
+ errors = response.errors;
+ break;
+ }
+ } else {
+ data = response.data;
+ }
+
+ return QueryResult(
+ data: data,
+ context: response.context,
+ source: source,
+ exception: coalesceErrors(graphqlErrors: errors),
+ parserFn: options.parserFn,
+ );
+}

original_message: fix: Cache-reboardcast error


message: fix: Fetch more is broken with `parserFn`

diff:
@@ -21,18 +21,10 @@ Future<QueryResult<TParsed>> fetchMoreImplementation<TParsed>(
}) async {
// fetch more and update
- final document = (fetchMoreOptions.document ?? originalOptions.document);
final request = originalOptions.asRequest;
- final combinedOptions = QueryOptions<TParsed>(
- fetchPolicy: FetchPolicy.noCache,
- errorPolicy: originalOptions.errorPolicy,
- document: document,
- variables: {
- ...originalOptions.variables,
- ...fetchMoreOptions.variables,
- },
- );
+ final combinedOptions =
+ originalOptions.withFetchMoreOptions(fetchMoreOptions);
QueryResult<TParsed> fetchMoreResult =
await queryManager.query(combinedOptions);

diff:
+import 'package:gql/language.dart';
import 'package:graphql/src/core/_base_options.dart';
import 'package:graphql/src/core/result_parser.dart';
import 'package:graphql/src/utilities/helpers.dart';
@@ -42,6 +43,22 @@ class QueryOptions<TParsed> extends BaseOptions<TParsed> {
pollInterval,
];
+ QueryOptions<TParsed> withFetchMoreOptions(
+ FetchMoreOptions fetchMoreOptions,
+ ) =>
+ QueryOptions<TParsed>(
+ document: fetchMoreOptions.document ?? document,
+ operationName: operationName,
+ fetchPolicy: FetchPolicy.noCache,
+ errorPolicy: errorPolicy,
+ parserFn: parserFn,
+ context: context,
+ variables: {
+ ...variables,
+ ...fetchMoreOptions.variables,
+ },
+ );
+
WatchQueryOptions<TParsed> asWatchQueryOptions({bool fetchResults = true}) =>
WatchQueryOptions(
document: document,
@@ -54,7 +71,7 @@ class QueryOptions<TParsed> extends BaseOptions<TParsed> {
fetchResults: fetchResults,
context: context,
optimisticResult: optimisticResult,
- parserFn: this.parserFn,
+ parserFn: parserFn,
);
QueryOptions<TParsed> copyWithPolicies(Policies policies) => QueryOptions(

diff:
@@ -222,6 +222,76 @@ void main() {
equals('bar'),
);
});
+ test('successful fetch-more with parser', () async {
+ final ResultParserFn<List<String>> parserFn = (data) {
+ return data['viewer']['repositories']['nodes']
+ .map<String>((node) => node['name'] as String)
+ .toList();
+ };
+ final _options = QueryOptions(
+ document: parseString(readRepositories),
+ variables: <String, dynamic>{
+ 'nRepositories': 42,
+ },
+ parserFn: parserFn,
+ );
+ final repoData = readRepositoryData(withTypenames: true);
+
+ when(
+ link.request(any),
+ ).thenAnswer(
+ (_) => Stream.fromIterable([
+ Response(
+ data: repoData,
+ context: Context().withEntry(
+ HttpLinkResponseContext(
+ statusCode: 200,
+ headers: {'foo': 'bar'},
+ ),
+ ),
+ ),
+ ]),
+ );
+
+ final QueryResult<List<String>> r1 = await client.query(_options);
+ expect(r1.exception, isNull);
+ expect(r1.data, equals(repoData));
+ expect(
+ r1.parsedData,
+ equals([
+ 'pq',
+ 'go-evercookie',
+ 'watchbot',
+ ]));
+ final QueryResult<List<String>> r2 = await client.fetchMore(
+ FetchMoreOptions(
+ updateQuery: (d1, d2) => ({
+ 'viewer': {
+ 'repositories': {
+ 'nodes': [
+ ...d1!['viewer']['repositories']['nodes'],
+ ...d2!['viewer']['repositories']['nodes'],
+ ]
+ }
+ }
+ }),
+ ),
+ previousResult: r1,
+ originalOptions: _options,
+ );
+
+ expect(r2.exception, isNull);
+ expect(
+ r2.parsedData,
+ equals([
+ 'pq',
+ 'go-evercookie',
+ 'watchbot',
+ 'pq',
+ 'go-evercookie',
+ 'watchbot',
+ ]));
+ });
test('successful response without normalization', () async {
final readUnidentifiedRepositories = parseString(r'''


message: fix: Stream broadcast

diff:
@@ -92,7 +92,9 @@ class QueryManager {
}
try {
- yield* link.request(request).map((response) {
+ yield* link
+ .request(request)
+ .map((response) {
QueryResult<TParsed>? queryResult;
bool rereadFromCache = false;
try {
@@ -127,13 +129,18 @@ class QueryManager {
}
return queryResult;
- }).transform(StreamTransformer.fromHandlers(
+ })
+ .transform<QueryResult<TParsed>>(StreamTransformer.fromHandlers(
handleError: (err, trace, sink) => sink.add(_wrapFailure(
err,
trace,
options.parserFn,
)),
- ));
+ ))
+ .map((QueryResult<TParsed> queryResult) {
+ maybeRebroadcastQueries();
+ return queryResult;
+ });
} catch (ex, trace) {
yield* Stream.fromIterable([
_wrapFailure(

diff:
@@ -657,29 +657,109 @@ void main() {
),
);
});
- test('parses results', () async {
- final responses = [
- {
- 'id': '1',
- 'name': 'first',
- },
- {
- 'id': '2',
- 'name': 'second',
- },
- ].map((item) => Response(
+ test('broadcasts', () async {
+ const initialName = 'initial';
+ const firstUpdateName = 'first';
+ const secondUpdateName = 'second';
+ final initialQueryResponse = Response(
data: <String, dynamic>{
- 'item': {
+ 'single': {
+ 'id': '1',
'__typename': 'Item',
- ...item,
+ 'name': initialName,
},
},
- ));
+ );
+ when(
+ link.request(any),
+ ).thenAnswer(
+ (_) => Stream.fromIterable(
+ [initialQueryResponse],
+ ),
+ );
+
+ final ObservableQuery observable = client.watchQuery(
+ WatchQueryOptions(
+ document: parseString(readSingle),
+ eagerlyFetchResults: true,
+ variables: {'id': '1'},
+ ),
+ );
+ final responses = [
+ {'id': '1', 'name': firstUpdateName, '__typename': 'Item'},
+ {'id': '1', 'name': secondUpdateName, '__typename': 'Item'},
+ ].map((item) => Response(data: <String, dynamic>{'item': item}));
when(
link.request(any),
).thenAnswer(
(_) => Stream.fromIterable(responses),
);
+ final stream = client.subscribe(
+ SubscriptionOptions(
+ document: parseString(
+ r'''
+ subscription {
+ item {
+ id
+ name
+ }
+ }
+ ''',
+ ),
+ ),
+ );
+ expect(
+ stream,
+ emitsInOrder(
+ [
+ isA<QueryResult>().having(
+ (result) => result.data!['item']['name'],
+ 'name',
+ firstUpdateName,
+ ),
+ isA<QueryResult>().having(
+ (result) => result.data!['item']['name'],
+ 'name',
+ secondUpdateName,
+ )
+ ],
+ ),
+ );
+ expect(
+ observable.stream,
+ emitsInOrder(
+ [
+ isA<QueryResult>().having(
+ (result) => result.data,
+ 'name',
+ null,
+ ),
+ isA<QueryResult>().having(
+ (result) => result.data!['single']['name'],
+ 'name',
+ initialName,
+ ),
+ isA<QueryResult>().having(
+ (result) => result.data!['single']['name'],
+ 'name',
+ firstUpdateName,
+ ),
+ isA<QueryResult>().having(
+ (result) => result.data!['single']['name'],
+ 'name',
+ secondUpdateName,
+ )
+ ],
+ ),
+ );
+ });
+ test('parses results', () async {
+ final responses = [
+ {'id': '1', 'name': 'first', '__typename': 'Item'},
+ {'id': '2', 'name': 'second', '__typename': 'Item'},
+ ].map((item) => Response(data: <String, dynamic>{'item': item}));
+ when(link.request(any))
+ .thenAnswer((_) => Stream.fromIterable(responses));
final ResultParserFn<String> parserFn =
(data) => data['item']['name'] as String;

original_message: fix: Stream broadcast


message:
docs: Improve documentation
Added documentation on code-generation and hooks.

diff:
@@ -41,7 +41,7 @@ As of `v4`, it is built on foundational libraries from the [gql-dart project], i
- [Links](#links)
- [Composing Links](#composing-links)
- [AWS AppSync Support](#aws-appsync-support)
- - [Parsing ASTs at build-time](#parsing-asts-at-build-time)
+ - [Code generation](#code-generation)
- [`PersistedQueriesLink` (experimental) :warning: OUT OF SERVICE :warning:](#persistedquerieslink-experimental-warning-out-of-service-warning)
**Useful API Docs:**
@@ -768,21 +768,17 @@ API key, IAM, and Federated provider authorization could be accomplished through
- Making a custom link: [Comment on Issue 173](https://github.com/zino-app/graphql-flutter/issues/173#issuecomment-464435942)
- AWS JS SDK `auth-link.ts`: [aws-mobile-appsync-sdk-js:auth-link.ts](https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/master/packages/aws-appsync-auth-link/src/auth-link.ts)
-## Parsing ASTs at build-time
+## Code generation
-All `document` arguments are `DocumentNode`s from `gql/ast`.
-We supply a `gql` helper for parsing, them, but you can also
-parse documents at build-time use `ast_builder` from
-[`package:gql_code_gen`](https://pub.dev/packages/gql_code_gen):
+This package does not support code-generation out of the box, but [graphql_codegen](https://pub.dev/packages/graphql_codegen) does!
-```yaml
-dev_dependencies:
- gql_code_gen: ^0.1.5
-```
+This package extensions on the client which takes away the struggle of serialization and gives you confidence through type-safety.
+It is also more performant than parsing GraphQL queries at runtime.
-**`add_star.graphql`**:
+For example, by creating the `.graphql` file
```graphql
+# add_star.graphql
mutation AddStar($starrableId: ID!) {
action: addStar(input: { starrableId: $starrableId }) {
starrable {
@@ -792,19 +788,20 @@ mutation AddStar($starrableId: ID!) {
}
```
+after building, you'll be able to execute your mutation on the client as:
+
```dart
-import 'package:gql/add_star.ast.g.dart' as add_star;
+// add_star.dart
+import 'add_star.graphql.dart';
-// ...
+// ..
-final MutationOptions options = MutationOptions(
- document: add_star.document,
- variables: <String, dynamic>{
- 'starrableId': repositoryID,
- },
-);
-// ...
+ await client.mutateAddStar(
+ OptionsMutationAddStar(
+ variables: VariablesMutationAddStar(starableId: repositoryID)
+ )
+ );
```
## `PersistedQueriesLink` (experimental) :warning: OUT OF SERVICE :warning:


message: fix: Beta branch reliance

diff:
@@ -109,8 +109,7 @@ jobs:
- run:
name: Bump version numbers
command: |
- if [[ $CIRCLE_TAG =~ ^.*beta.*$ ]]; then BRANCH=beta; else BRANCH=beta; fi
- git checkout $BRANCH
+ git checkout main
git add packages/graphql/pubspec.yaml packages/graphql_flutter/pubspec.yaml
git commit --allow-empty -m "build(Pub): Bump version numbers [skip ci]"

original_message: fix: Beta branch reliance


message: fix: Add .pubignore and dependency

diff: +test

diff:
@@ -21,8 +21,5 @@ dev_dependencies:
sdk: flutter
http: ^0.13.0
test: ^1.17.12
-dependency_overrides:
- graphql:
- path: ../graphql
environment:
sdk: '>=2.12.0 <3.0.0'

original_message: fix: Add .pubignore and dependency


message:
graphql: Create result from options
Changelog-Add: Create result from options

diff:
@@ -115,4 +115,18 @@ abstract class BaseOptions<TParsed extends Object?> {
int get hashCode => const ListEquality<Object?>(
DeepCollectionEquality(),
).hash(properties);
+
+ QueryResult<TParsed> createResult({
+ Map<String, dynamic>? data,
+ OperationException? exception,
+ Context context = const Context(),
+ required QueryResultSource source,
+ }) =>
+ QueryResult.internal(
+ parserFn: parserFn,
+ data: data,
+ exception: exception,
+ context: context,
+ source: source,
+ );
}

diff:
import 'dart:async';
import 'package:graphql/client.dart';
-import 'package:graphql/src/utilities/response.dart';
import 'package:meta/meta.dart';
import 'package:graphql/src/core/fetch_more.dart';
@@ -133,7 +132,7 @@ class ObservableQuery<TParsed> {
if (isRefetchSafe) {
addResult(QueryResult.loading(
data: latestResult?.data,
- parserFn: options.parserFn,
+ options: options,
));
return await queryManager.refetchQuery<TParsed>(queryId);
}
@@ -220,7 +219,7 @@ class ObservableQuery<TParsed> {
FetchMoreOptions fetchMoreOptions) async {
addResult(QueryResult.loading(
data: latestResult?.data,
- parserFn: options.parserFn,
+ options: options,
));
return fetchMoreImplementation(
@@ -232,17 +231,6 @@ class ObservableQuery<TParsed> {
);
}
- void addFetchResult(
- Response response,
- QueryResultSource source, {
- bool fromRebroadcast = false,
- }) {
- addResult(
- mapFetchResultToQueryResult<TParsed>(response, options, source: source),
- fromRebroadcast: fromRebroadcast,
- );
- }
-
/// Add a [result] to the [stream] unless it was created
/// before [lasestResult].
///

diff:
import 'dart:async';
-import 'package:graphql/src/core/result_parser.dart';
import 'package:graphql/src/utilities/response.dart';
import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
@@ -76,7 +75,7 @@ class QueryManager {
// TODO optimisticResults for streams just skip the cache for now
yield QueryResult.optimistic(
data: options.optimisticResult as Map<String, dynamic>?,
- parserFn: options.parserFn,
+ options: options,
);
} else if (shouldRespondEagerlyFromCache(options.fetchPolicy)) {
final cacheResult = cache.readQuery(
@@ -85,9 +84,9 @@ class QueryManager {
);
if (cacheResult != null) {
yield QueryResult(
+ options: options,
source: QueryResultSource.cache,
data: cacheResult,
- parserFn: options.parserFn,
);
}
}
@@ -114,8 +113,8 @@ class QueryManager {
} catch (failure, trace) {
// we set the source to indicate where the source of failure
queryResult ??= QueryResult(
+ options: options,
source: QueryResultSource.network,
- parserFn: options.parserFn,
);
queryResult.exception = coalesceErrors(
@@ -133,9 +132,9 @@ class QueryManager {
})
.transform<QueryResult<TParsed>>(StreamTransformer.fromHandlers(
handleError: (err, trace, sink) => sink.add(_wrapFailure(
+ options,
err,
trace,
- options.parserFn,
)),
))
.map((QueryResult<TParsed> queryResult) {
@@ -145,9 +144,9 @@ class QueryManager {
} catch (ex, trace) {
yield* Stream.fromIterable([
_wrapFailure(
+ options,
ex,
trace,
- options.parserFn,
)
]);
}
@@ -222,7 +221,7 @@ class QueryManager {
// _resolveQueryEagerly handles cacheOnly,
// so if we're loading + cacheFirst we continue to network
return MultiSourceResult(
- parserFn: options.parserFn,
+ options: options,
eagerResult: eagerResult,
networkResult:
(shouldStopAtCache(options.fetchPolicy) && !eagerResult.isLoading)
@@ -262,8 +261,8 @@ class QueryManager {
} catch (failure, trace) {
// we set the source to indicate where the source of failure
queryResult ??= QueryResult(
+ options: options,
source: QueryResultSource.network,
- parserFn: options.parserFn,
);
queryResult.exception = coalesceErrors(
@@ -295,9 +294,7 @@ class QueryManager {
String queryId,
BaseOptions<TParsed> options,
) {
- QueryResult<TParsed> queryResult = QueryResult.loading(
- parserFn: options.parserFn,
- );
+ QueryResult<TParsed> queryResult = QueryResult.loading(options: options);
try {
if (options.optimisticResult != null) {
@@ -317,17 +314,17 @@ class QueryManager {
// we only push an eager query with data
if (data != null) {
queryResult = QueryResult(
+ options: options,
data: data,
source: QueryResultSource.cache,
- parserFn: options.parserFn,
);
}
if (options.fetchPolicy == FetchPolicy.cacheOnly &&
queryResult.isLoading) {
queryResult = QueryResult(
+ options: options,
source: QueryResultSource.cache,
- parserFn: options.parserFn,
exception: OperationException(
linkException: CacheMissException(
'Could not resolve the given request against the cache. (FetchPolicy.cacheOnly)',
@@ -363,8 +360,7 @@ class QueryManager {
/// Refetch the [ObservableQuery] referenced by [queryId],
/// overriding any present non-network-only [FetchPolicy].
Future<QueryResult<TParsed>?> refetchQuery<TParsed>(String queryId) {
- WatchQueryOptions<TParsed> options =
- queries[queryId]!.options as WatchQueryOptions<TParsed>;
+ var options = getQuery<TParsed>(queryId)!.options;
if (!willAlwaysExecuteOnNetwork(options.fetchPolicy)) {
options = options.copyWithFetchPolicy(FetchPolicy.networkOnly);
}
@@ -387,10 +383,13 @@ class QueryManager {
}
ObservableQuery<Object?>? getQuery(String? queryId) {
- if (queries.containsKey(queryId)) {
- return queries[queryId!];
+ if (!queries.containsKey(queryId)) {
+ return null;
+ }
+ final query = queries[queryId!];
+ if (query is ObservableQuery<TParsed>) {
+ return query;
}
-
return null;
}
@@ -404,8 +403,7 @@ class QueryManager {
String? queryId,
QueryResult<TParsed> queryResult,
) {
- final ObservableQuery<TParsed>? observableQuery =
- getQuery(queryId) as ObservableQuery<TParsed>?;
+ final observableQuery = getQuery<TParsed>(queryId);
if (observableQuery != null && !observableQuery.controller.isClosed) {
observableQuery.addResult(queryResult);
@@ -420,8 +418,8 @@ class QueryManager {
required BaseOptions<TParsed> options,
}) {
QueryResult<TParsed> queryResult = QueryResult(
+ options: options,
source: QueryResultSource.optimisticResult,
- parserFn: options.parserFn,
);
attemptCacheWriteFromClient(
@@ -477,9 +475,12 @@ class QueryManager {
optimistic: query.options.policies.mergeOptimisticData,
);
if (_cachedDataHasChangedFor(query, cachedData)) {
- query.addFetchResult(
+ query.addResult(
+ mapFetchResultToQueryResult(
Response(data: cachedData, response: {}),
- QueryResultSource.cache,
+ query.options,
+ source: QueryResultSource.cache,
+ ),
fromRebroadcast: true,
);
}
@@ -517,13 +518,13 @@ class QueryManager {
}
QueryResult<TParsed> _wrapFailure<TParsed>(
+ BaseOptions<TParsed> options,
dynamic ex,
StackTrace trace,
- ResultParserFn<TParsed> parserFn,
) =>
QueryResult(
+ options: options,
// we set the source to indicate where the source of failure
source: QueryResultSource.network,
exception: coalesceErrors(linkException: translateFailure(ex, trace)),
- parserFn: parserFn,
);

diff:
import 'dart:async' show FutureOr;
import 'package:graphql/client.dart';
+import 'package:graphql/src/core/_base_options.dart';
import 'package:graphql/src/core/result_parser.dart';
+import 'package:meta/meta.dart';
/// The source of the result data contained
///
@@ -39,7 +41,8 @@ final _eagerSources = {
/// A single operation result
class QueryResult<TParsed extends Object?> {
- QueryResult({
+ @protected
+ QueryResult.internal({
this.data,
this.exception,
this.context = const Context(),
@@ -47,32 +50,44 @@ class QueryResult<TParsed extends Object?> {
required this.source,
}) : timestamp = DateTime.now();
+ factory QueryResult({
+ required BaseOptions<TParsed> options,
+ Map<String, dynamic>? data,
+ OperationException? exception,
+ Context context = const Context(),
+ required QueryResultSource source,
+ }) =>
+ options.createResult(
+ source: source,
+ data: data,
+ exception: exception,
+ context: context,
+ );
+
/// Unexecuted singleton, used as a placeholder for mutations,
/// etc.
- static final unexecuted = QueryResult(
+ static final unexecuted = QueryResult.internal(
source: null,
parserFn: (d) =>
throw UnimplementedError("Unexecuted query data can not be parsed."),
)..timestamp = DateTime.fromMillisecondsSinceEpoch(0);
factory QueryResult.loading({
+ required BaseOptions<TParsed> options,
Map<String, dynamic>? data,
- required ResultParserFn<TParsed> parserFn,
}) =>
- QueryResult(
+ options.createResult(
data: data,
source: QueryResultSource.loading,
- parserFn: parserFn,
);
factory QueryResult.optimistic({
+ required BaseOptions<TParsed> options,
Map<String, dynamic>? data,
- required ResultParserFn<TParsed> parserFn,
}) =>
- QueryResult(
+ options.createResult(
data: data,
source: QueryResultSource.optimisticResult,
- parserFn: parserFn,
);
DateTime timestamp;
@@ -141,10 +156,10 @@ class QueryResult<TParsed extends Object?> {
class MultiSourceResult<TParsed> {
MultiSourceResult({
+ required BaseOptions<TParsed> options,
QueryResult<TParsed>? eagerResult,
this.networkResult,
- required ResultParserFn<TParsed> parserFn,
- }) : eagerResult = eagerResult ?? QueryResult.loading(parserFn: parserFn),
+ }) : eagerResult = eagerResult ?? QueryResult.loading(options: options),
assert(
eagerResult!.source != QueryResultSource.network,
'An eager result cannot be gotten from the network',

diff:
@@ -35,10 +35,10 @@ QueryResult<TParsed> mapFetchResultToQueryResult<TParsed>(
}
return QueryResult(
+ options: options,
data: data,
context: response.context,
source: source,
exception: coalesceErrors(graphqlErrors: errors),
- parserFn: options.parserFn,
);
}

diff:
+import 'package:gql/language.dart';
+import 'package:graphql/client.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+
+import 'helpers.dart';
+
+void main() {
+ late MockLink link;
+ late GraphQLClient client;
+
+ setUp(() {
+ link = MockLink();
+
+ client = GraphQLClient(
+ cache: getTestCache(),
+ link: link,
+ );
+ });
+
+ group('QueryManager', () {
+ test("Can refetch", () {
+ final response = Response(
+ data: <String, dynamic>{
+ 'fetchPerson': null,
+ },
+ response: {},
+ );
+ when(
+ link.request(any),
+ ).thenAnswer(
+ (_) => Stream.fromIterable(
+ [response],
+ ),
+ );
+
+ final observable = client.watchQuery(
+ WatchQueryOptions<String?>(
+ document: parseString("""{ fetchPerson { name } }"""),
+ parserFn: (data) => data['fetchPerson']?['name'] as String?,
+ ),
+ );
+ client.queryManager.refetchQuery(observable.queryId);
+ });
+ });
+}

diff:
@@ -30,9 +30,9 @@ QueryResult<TParsed> useSubscription<TParsed>(
initialData: options.optimisticResult != null
? QueryResult.optimistic(
data: options.optimisticResult as Map<String, dynamic>?,
- parserFn: options.parserFn,
+ options: options,
)
- : QueryResult.loading(parserFn: options.parserFn),
+ : QueryResult.loading(options: options),
);
return snapshot.data!;
}


message: fix(graphql): Hive integration

diff:
@@ -13,9 +13,9 @@ class HiveStore extends Store {
/// Opens a box. Convenience pass through to [Hive.openBox].
///
/// If the box is already open, the instance is returned and all provided parameters are being ignored.
- static Future<Box<Map<String, dynamic>>> openBox(
- {required String boxName, String? path}) async {
- return await Hive.openBox<Map<String, dynamic>>(boxName, path: path);
+ static Future<Box<Map<dynamic, dynamic>?>> openBox(String boxName,
+ {String? path}) async {
+ return await Hive.openBox<Map<dynamic, dynamic>?>(boxName, path: path);
}
/// Convenience factory for `HiveStore(await openBox(boxName ?? 'graphqlClientStore', path: path))`
@@ -26,7 +26,7 @@ class HiveStore extends Store {
String boxName = defaultBoxName,
String? path,
}) async =>
- HiveStore(await openBox(boxName: boxName, path: path));
+ HiveStore(await openBox(boxName, path: path));
/// Init Hive on specific Path
static void init({required String onPath}) => Hive.init(onPath);
@@ -35,7 +35,7 @@ class HiveStore extends Store {
///
/// **WARNING**: Directly editing the contents of the store will not automatically
/// rebroadcast operations.
- final Box<Map<String, dynamic>?> box;
+ final Box<Map<dynamic, dynamic>?> box;
/// Creates a HiveStore initialized with the given [box], defaulting to `Hive.box(defaultBoxName)`
///
@@ -43,14 +43,14 @@ class HiveStore extends Store {
/// This lets us decouple the async initialization logic, making store usage elsewhere much more straightforward.
///
/// [opened]: https://docs.hivedb.dev/#/README?id=open-a-box
- HiveStore([Box<Map<String, dynamic>>? box])
- : this.box = box ?? Hive.box<Map<String, dynamic>>(defaultBoxName);
+ HiveStore([Box<Map<dynamic, dynamic>?>? box])
+ : this.box = box ?? Hive.box<Map<dynamic, dynamic>?>(defaultBoxName);
@override
Map<String, dynamic>? get(String dataId) {
final result = box.get(dataId);
if (result == null) return null;
- return Map.from(result);
+ return Map<String, dynamic>.from(result);
}
@override
@@ -59,7 +59,7 @@ class HiveStore extends Store {
}
@override
- void putAll(Map<String, Map<String, dynamic>> data) {
+ void putAll(Map<String, Map<String, dynamic>?> data) {
box.putAll(data);
}
@@ -69,7 +69,7 @@ class HiveStore extends Store {
}
@override
- Map<String, Map<String, dynamic>> toMap() => Map.unmodifiable(box.toMap());
+ Map<String, Map<String, dynamic>?> toMap() => Map.unmodifiable(box.toMap());
Future<void> reset() => box.clear();
}

diff:
@@ -15,7 +15,7 @@ abstract class Store {
/// [put] all entries from [data] into the store
///
/// Functionally equivalent to `data.map(put);`
- void putAll(Map<String, Map<String, dynamic>> data);
+ void putAll(Map<String, Map<String, dynamic>?> data);
/// Delete the value of the [dataId] from the store, if preset
void delete(String dataId);
@@ -27,7 +27,7 @@ abstract class Store {
///
/// NOTE: some [Store]s might return mutable objects
/// referenced by the store itself.
- Map<String, Map<String, dynamic>> toMap();
+ Map<String, Map<String, dynamic>?> toMap();
}
/// Simplest possible [Map]-backed store
@@ -52,7 +52,7 @@ class InMemoryStore extends Store {
void put(String dataId, Map<String, dynamic>? value) => data[dataId] = value;
@override
- void putAll(Map<String, Map<String, dynamic>> entries) =>
+ void putAll(Map<String, Map<String, dynamic>?> entries) =>
data.addAll(entries);
@override
@@ -60,7 +60,7 @@ class InMemoryStore extends Store {
/// Return the underlying [data] as an unmodifiable [Map].
@override
- Map<String, Map<String, dynamic>> toMap() => Map.unmodifiable(data);
+ Map<String, Map<String, dynamic>?> toMap() => Map.unmodifiable(data);
void reset() => data.clear();
}

diff:
@@ -71,6 +71,49 @@ void main() {
await store.box.deleteFromDisk();
});
+ group("Re-open store works", () {
+ test("Can re-open store", () async {
+ final box1 = await HiveStore.openBox(
+ 're-open-store',
+ path: path,
+ );
+ final store = HiveStore(box1);
+ store.put("id", {'foo': 'bar'});
+ final readData = await store.get("id");
+ expect(readData, equals({'foo': 'bar'}));
+ expect(readData, isA<Map<String, dynamic>>());
+ await box1.close();
+ final box2 = await HiveStore.openBox(
+ 're-open-store',
+ path: path,
+ );
+ final store2 = HiveStore(box2);
+ final readData2 = await store2.get('id');
+ expect(readData2, equals({'foo': 'bar'}));
+ expect(readData2, isA<Map<String, dynamic>>());
+ });
+ test("Can put null", () async {
+ final box1 = await HiveStore.openBox(
+ 'put-null',
+ path: path,
+ );
+ final store = HiveStore(box1);
+ store.put("id", {'foo': 'bar'});
+ store.put("id", null);
+ final readData = await store.get("id");
+ expect(readData, equals(null));
+ await box1.close();
+ final box2 = await HiveStore.openBox(
+ 'put-null',
+ path: path,
+ );
+ final store2 = HiveStore(box2);
+ final readData2 = await store2.get('id');
+ expect(readData2, equals(null));
+ expect(store2.toMap(), isA<Map<String, Map<String, dynamic>?>>());
+ expect(store2.toMap(), equals({'id': null}));
+ });
+ });
tearDownAll(() async {
await Directory(path).delete(recursive: true);

original_message: fix(graphql): Hive integration


message: feat(graphql): Tighten the OnMutationCompleted function

diff:
@@ -7,7 +7,8 @@ import 'package:graphql/src/core/result_parser.dart';
import 'package:meta/meta.dart';
-typedef OnMutationCompleted = FutureOr<void> Function(dynamic data);
+typedef OnMutationCompleted = FutureOr<void> Function(
+ Map<String, dynamic>? data);
typedef OnMutationUpdate<TParsed> = FutureOr<void> Function(
GraphQLDataProxy cache,
QueryResult<TParsed>? result,


message: graphql: Prepare release 5.1.1

diff:
+# v5.1.1
+
+## New Feature
+- Tighten the OnMutationCompleted function ([commit](https://github.com/zino-hofmann/graphql-flutter/commit/59ebaa94d44a531416687904788b823ef5c17b5d)). @budde377 09-04-2022
+
+
# v5.1.0
## New Feature

diff:
{
"package_name": "graphql",
- "version": "v5.1.0",
+ "version": "v5.1.1",
"api": {
"name": "github",
"repository": "zino-hofmann/graphql-flutter",

diff:
name: graphql
description: A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package.
-version: 5.1.0
+version: 5.1.1
homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql
dependencies:
meta: ^1.3.0

original_message: graphql: Prepare release 5.1.1


message: feat(graphql_flutter): Decouple GraphQLClient Provider

diff:
@@ -20,13 +20,20 @@ class MutationHookResult<TParsed> {
MutationHookResult<TParsed> useMutation<TParsed>(
MutationOptions<TParsed> options,
+) {
+ final client = useGraphQLClient();
+ return useMutationOnClient(client, options);
+}
+
+MutationHookResult<TParsed> useMutationOnClient<TParsed>(
+ GraphQLClient client,
+ MutationOptions<TParsed> options,
) {
final watchOptions = useMemoized(
() => options.asWatchQueryOptions(),
[options],
);
- final client = useGraphQLClient();
- final query = useWatchMutation<TParsed>(watchOptions);
+ final query = useWatchMutationOnClient<TParsed>(client, watchOptions);
final snapshot = useStream(
query.stream,
initialData: query.latestResult ?? QueryResult.unexecuted,

diff:
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql/client.dart';
+import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
import 'package:graphql_flutter/src/widgets/hooks/watch_query.dart';
// method to call from widget to fetchmore queries
@@ -22,11 +23,19 @@ class QueryHookResult<TParsed> {
}
QueryHookResult<TParsed> useQuery<TParsed>(QueryOptions<TParsed> options) {
+ final client = useGraphQLClient();
+ return useQueryOnClient(client, options);
+}
+
+QueryHookResult<TParsed> useQueryOnClient<TParsed>(
+ GraphQLClient client,
+ QueryOptions<TParsed> options,
+) {
final watchQueryOptions = useMemoized(
() => options.asWatchQueryOptions(),
[options],
);
- final query = useWatchQuery(watchQueryOptions);
+ final query = useWatchQueryOnClient(client, watchQueryOptions);
final snapshot = useStream(
query.stream,
initialData: query.latestResult,

diff:
@@ -20,6 +20,18 @@ QueryResult<TParsed> useSubscription<TParsed>(
OnSubscriptionResult<TParsed>? onSubscriptionResult,
}) {
final client = useGraphQLClient();
+ return useSubscriptionOnClient(
+ client,
+ options,
+ onSubscriptionResult: onSubscriptionResult,
+ );
+}
+
+QueryResult<TParsed> useSubscriptionOnClient<TParsed>(
+ GraphQLClient client,
+ SubscriptionOptions<TParsed> options, {
+ OnSubscriptionResult<TParsed>? onSubscriptionResult,
+}) {
final stream = use(_SubscriptionHook(
client: client,
onSubscriptionResult: onSubscriptionResult,

diff:
@@ -66,6 +66,13 @@ ObservableQuery<TParsed> useWatchQuery<TParsed>(
WatchQueryOptions<TParsed> options,
) {
final client = useGraphQLClient();
+ return useWatchQueryOnClient(client, options);
+}
+
+ObservableQuery<TParsed> useWatchQueryOnClient<TParsed>(
+ GraphQLClient client,
+ WatchQueryOptions<TParsed> options,
+) {
final overwrittenOptions = useMemoized(() {
final policies =
client.defaultPolicies.query.withOverrides(options.policies);
@@ -79,8 +86,16 @@ ObservableQuery<TParsed> useWatchQuery<TParsed>(
}
ObservableQuery<TParsed> useWatchMutation<TParsed>(
- WatchQueryOptions<TParsed> options) {
+ WatchQueryOptions<TParsed> options,
+) {
final client = useGraphQLClient();
+ return useWatchMutationOnClient(client, options);
+}
+
+ObservableQuery<TParsed> useWatchMutationOnClient<TParsed>(
+ GraphQLClient client,
+ WatchQueryOptions<TParsed> options,
+) {
final overwrittenOptions = useMemoized(() {
final policies =
client.defaultPolicies.mutate.withOverrides(options.policies);

diff:
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql/client.dart';
+import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
import 'package:graphql_flutter/src/widgets/hooks/mutation.dart';
typedef MutationBuilder<TParsed> = Widget Function(
@@ -9,7 +10,7 @@ typedef MutationBuilder<TParsed> = Widget Function(
);
/// Builds a [Mutation] widget based on the a given set of [MutationOptions]
-/// that streams [QueryResult]s into the [QueryBuilder].
+/// that streams [QueryResult]s into the [MutationBuilder].
class Mutation<TParsed> extends HookWidget {
const Mutation({
final Key? key,
@@ -22,7 +23,32 @@ class Mutation<TParsed> extends HookWidget {
@override
Widget build(BuildContext context) {
- final result = useMutation(options);
+ final client = useGraphQLClient();
+ return MutationOnClient(
+ client: client,
+ options: options,
+ builder: builder,
+ );
+ }
+}
+
+/// Builds a [MutationOnClient] widget based on the a given set of [MutationOptions]
+/// that streams [QueryResult]s into the [MutationBuilder].
+class MutationOnClient<TParsed> extends HookWidget {
+ const MutationOnClient({
+ final Key? key,
+ required this.client,
+ required this.options,
+ required this.builder,
+ }) : super(key: key);
+
+ final GraphQLClient client;
+ final MutationOptions<TParsed> options;
+ final MutationBuilder<TParsed> builder;
+
+ @override
+ Widget build(BuildContext context) {
+ final result = useMutationOnClient(client, options);
return builder(result.runMutation, result.result);
}
}

diff:
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql/client.dart';
+import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
import 'package:graphql_flutter/src/widgets/hooks/query.dart';
typedef QueryBuilder<TParsed> = Widget Function(
@@ -23,7 +24,32 @@ class Query<TParsed> extends HookWidget {
@override
Widget build(BuildContext context) {
- final result = useQuery(options);
+ final client = useGraphQLClient();
+ return QueryOnClient(
+ options: options,
+ builder: builder,
+ client: client,
+ );
+ }
+}
+
+/// Builds a [QueryOnClient] widget based on the a given set of [QueryOptions]
+/// that streams [QueryResult]s into the [QueryBuilder].
+class QueryOnClient<TParsed> extends HookWidget {
+ const QueryOnClient({
+ final Key? key,
+ required this.options,
+ required this.builder,
+ required this.client,
+ }) : super(key: key);
+
+ final GraphQLClient client;
+ final QueryOptions<TParsed> options;
+ final QueryBuilder<TParsed> builder;
+
+ @override
+ Widget build(BuildContext context) {
+ final result = useQueryOnClient(client, options);
return builder(
result.result,
fetchMore: result.fetchMore,

diff:
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:graphql/client.dart';
+import 'package:graphql_flutter/src/widgets/hooks/graphql_client.dart';
import 'package:graphql_flutter/src/widgets/hooks/subscription.dart';
/// Creats a subscription with [GraphQLClient.subscribe].
@@ -67,7 +68,35 @@ class Subscription<TParsed> extends HookWidget {
@override
Widget build(BuildContext context) {
- final result = useSubscription<TParsed>(
+ final client = useGraphQLClient();
+ return SubscriptionOnClient(
+ client: client,
+ options: options,
+ builder: builder,
+ );
+ }
+}
+
+/// Creats a subscription widget like [Subscription] but
+/// with an external client.
+class SubscriptionOnClient<TParsed> extends HookWidget {
+ const SubscriptionOnClient({
+ required this.client,
+ required this.options,
+ required this.builder,
+ this.onSubscriptionResult,
+ Key? key,
+ }) : super(key: key);
+
+ final GraphQLClient client;
+ final SubscriptionOptions<TParsed> options;
+ final SubscriptionBuilder<TParsed> builder;
+ final OnSubscriptionResult<TParsed>? onSubscriptionResult;
+
+ @override
+ Widget build(BuildContext context) {
+ final result = useSubscriptionOnClient<TParsed>(
+ client,
options,
onSubscriptionResult: onSubscriptionResult,
);


message: fix(graphql): Minor formatting issues

diff:
@@ -324,7 +324,7 @@ class FetchMoreOptions {
}
/// merge fetchMore result data with earlier result data
-typedef Map<String, dynamic>? UpdateQuery(
+typedef UpdateQuery = Map<String, dynamic>? Function(
Map<String, dynamic>? previousResultData,
Map<String, dynamic>? fetchMoreResultData,
);

diff:
@@ -7,7 +7,9 @@ Future<WebSocketChannel> defaultConnectPlatform(
if (headers != null) {
print("The headers on the web are not supported");
}
- final webSocketChannel =
- await WebSocketChannel.connect(uri, protocols: protocols);
+ final webSocketChannel = WebSocketChannel.connect(
+ uri,
+ protocols: protocols,
+ );
return webSocketChannel.forGraphQL();
}

original_message: fix(graphql): Minor formatting issues


message: fix(graphql): Upgrade graphql version

diff:
@@ -4,7 +4,7 @@ version: 5.1.1-beta.1
homepage: https://github.com/zino-app/graphql-flutter/tree/master/packages/graphql_flutter
dependencies:
- graphql: ^5.1.1-beta.1
+ graphql: ^5.1.2-beta.1
gql_exec: ^0.4.0
flutter:
sdk: flutter

original_message: fix(graphql): Upgrade graphql version


message: fix(graphql): Transform nested objects

diff:
@@ -5,6 +5,29 @@ import 'package:hive/hive.dart';
import './store.dart';
+Map<String, dynamic> _transformMap(Map<dynamic, dynamic> map) =>
+ Map<String, dynamic>.from(
+ {
+ for (var entry in map.entries) entry.key: _transformAny(entry.value),
+ },
+ );
+
+dynamic _transformAny(dynamic object) {
+ if (object is Map) {
+ return _transformMap(object);
+ }
+ if (object is List) {
+ return _transformList(object);
+ }
+ return object;
+}
+
+List<dynamic> _transformList(List<dynamic> list) => List<dynamic>.from(
+ [
+ for (var element in list) _transformAny(element),
+ ],
+ );
+
@immutable
class HiveStore extends Store {
/// Default box name for the `graphql/client.dart` cache store (`graphqlClientStore`)
@@ -50,7 +73,7 @@ class HiveStore extends Store {
Map<String, dynamic>? get(String dataId) {
final result = box.get(dataId);
if (result == null) return null;
- return Map<String, dynamic>.from(result);
+ return _transformMap(result);
}
@override

diff:
@@ -92,6 +92,35 @@ void main() {
expect(readData2, equals({'foo': 'bar'}));
expect(readData2, isA<Map<String, dynamic>>());
});
+ test("Can re-open and read nested data", () async {
+ final box1 = await HiveStore.openBox(
+ 're-open-store',
+ path: path,
+ );
+ final store = HiveStore(box1);
+ final data = {
+ 'foo': 'bar',
+ 'bob': [
+ {'nested': true}
+ ]
+ };
+ store.put("id", data);
+ final readData = await store.get("id");
+ expect(readData, equals(data));
+ expect(readData?['bob'], isA<List<dynamic>>());
+ expect(readData?['bob'][0], isA<Map<String, dynamic>>());
+ await box1.close();
+ final box2 = await HiveStore.openBox(
+ 're-open-store',
+ path: path,
+ );
+ final store2 = HiveStore(box2);
+ final readData2 = await store2.get('id');
+ expect(readData2, equals(data));
+ expect(readData2, isA<Map<String, dynamic>>());
+ expect(readData2?['bob'], isA<List<dynamic>>());
+ expect(readData2?['bob'][0], isA<Map<String, dynamic>>());
+ });
test("Can put null", () async {
final box1 = await HiveStore.openBox(
'put-null',

original_message: fix(graphql): Transform nested objects


message: fix(graphql): Normalization signature

diff:
@@ -65,7 +65,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
/// The key differentiating factor for an implementing `cache` or `proxy`
/// is usually how they handle [optimistic] reads.
@protected
- Map<String, dynamic>? readNormalized(String rootId, {bool? optimistic});
+ Map<String, dynamic>? readNormalized(String rootId, {bool optimistic});
/// Write normalized data into the cache.
///
@@ -80,7 +80,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
Map<String, dynamic>? readQuery(
Request request, {
- bool? optimistic = true,
+ bool optimistic = true,
}) =>
denormalizeOperation(
// provided from cache
@@ -100,7 +100,7 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
Map<String, dynamic>? readFragment(
FragmentRequest fragmentRequest, {
- bool? optimistic = true,
+ bool optimistic = true,
}) =>
denormalizeFragment(
// provided from cache

diff:
@@ -59,8 +59,8 @@ class OptimisticProxy extends NormalizingDataProxy {
@override
Map<String, dynamic>? readNormalized(String rootId,
- {bool? optimistic = true}) {
- if (!optimistic!) {
+ {bool optimistic = true}) {
+ if (!optimistic) {
return cache.readNormalized(rootId, optimistic: false);
}
// the cache calls `patch.data.containsKey(rootId)`,

diff:
@@ -98,7 +98,7 @@ import './fragment.dart';
/// ```
abstract class GraphQLDataProxy {
/// Reads a GraphQL query from the root query id.
- Map<String, dynamic>? readQuery(Request request, {bool? optimistic});
+ Map<String, dynamic>? readQuery(Request request, {bool optimistic});
/// Reads a GraphQL fragment from any arbitrary id.
///
@@ -107,7 +107,7 @@ abstract class GraphQLDataProxy {
/// to select the correct fragment.
Map<String, dynamic>? readFragment(
FragmentRequest fragmentRequest, {
- bool? optimistic,
+ bool optimistic,
});
/// Writes a GraphQL query to the root query id,

original_message: fix(graphql): Normalization signature


message: feat: Refactor GH actions

change_type: RENAME

diff:
-name: Packages sanity check
+name: Build
on:
push:
@@ -8,13 +8,21 @@ on:
jobs:
build:
+ strategy:
+ matrix:
+ channel:
+ - beta
+ - stable
+ - dev
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
with:
- sdk: stable
+ channel: ${{matrix.channel}}
- name: Install dependencies
run: make dep
- name: Code formatting check
- run: make ci
\ No newline at end of file
+ run: make ci_fmt_client
+ - name: Code formatting check (flutter)
+ run: make ci_fmt_flutter
\ No newline at end of file

diff:
-name: graphql Code coverage
+name: Codecoverage
on:
push:
@@ -18,9 +18,9 @@ jobs:
- name: Run tests with coverage
run: |
make dep
- make ci_coverage_client
- - name: Upload coverage file
- run: |
curl -Os https://uploader.codecov.io/latest/linux/codecov
chmod +x codecov
+ make ci_coverage_client
+ ./codecov
+ make ci_coverage_flutter
./codecov
\ No newline at end of file

change_type: DELETE

diff:
-name: graphql-flutter Dart Code sanity check
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: subosito/flutter-action@v1
- with:
- channel: 'stable'
- - run: make dep
- - run: make ci_fmt_flutter
\ No newline at end of file

diff:
-name: graphql-flutter Code coverage
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: subosito/flutter-action@v1
- with:
- channel: 'stable'
- flutter-version: '2.10.5'
- - run: |
- make dep
- make ci_coverage_flutter
- - name: Upload coverage file
- run: |
- curl -Os https://uploader.codecov.io/latest/linux/codecov
- chmod +x codecov
- ./codecov
\ No newline at end of file

diff:
-name: graphql_flutter tests case
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: subosito/flutter-action@v1
- with:
- channel: 'stable'
- flutter-version: '2.10.5'
- - name: Install dependencies
- run: make dep
- - name: Tests
- run: make ci_check_flutter
\ No newline at end of file

diff:
-name: graphql Test case
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: subosito/flutter-action@v1
- with:
- channel: 'stable'
- - name: Install dependencies
- run: make dep
- - name: Tests
- run: make ci_check_client
\ No newline at end of file

diff:
-name: Packages sanity check
-
-on:
- push:
- branches: [main]
- pull_request:
- branches: [main]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: subosito/flutter-action@v1
- with:
- sdk: stable
- flutter-version: '2.10.5'
- - name: Install dependencies
- run: make dep
- - name: Code formatting check
- run: make analyze
\ No newline at end of file

diff:
-name: graphql Release Package
+name: Release (dart)
on:
release:
@@ -8,8 +8,8 @@ on:
push:
workflow_run:
workflows:
- - "graphql Test case"
- - "graphql Dart Code sanity check"
+ - "Test"
+ - "Build"
branches: [ main ]
types:
- completed

diff:
-name: graphql-flutter Release Package
+name: Release (flutter)
on:
release:
@@ -8,9 +8,8 @@ on:
push:
workflow_run:
workflows:
- - "graphql-flutter Dart Code sanity check"
- - "graphql-flutter Tests case"
- - "graphql Release Package"
+ - "Build"
+ - "Test"
branches: [ main ]
types:
- completed

diff:
-name: publish packages to pub.dev
+name: Release (packages)
on:
release:

diff:
-name: graphql Dart Code sanity check
+name: Test
on:
push:
@@ -16,5 +16,7 @@ jobs:
channel: 'stable'
- name: Install dependencies
run: make dep
- - name: Code formatting check
- run: make ci_fmt_client
\ No newline at end of file
+ - name: Tests (dart)
+ run: make ci_check_client
+ - name: Tests (flutter)
+ run: make ci_check_flutter
\ No newline at end of file

old_path: .gitignore

new_path: .gitignore

diff:
@@ -15,6 +15,7 @@ ios/Flutter/Generated.xcconfig
ios/Runner/GeneratedPluginRegistrant.*
.flutter-plugins
pubspec.lock
+pubspec_overrides.yaml
# Coverage
coverage.json

diff:
@@ -11,23 +11,25 @@ scripts:
run: melos exec -c 1 -- "dart format --set-exit-if-changed . && dart analyze . --fatal-infos"
description: Run dart analyzer in a specific package.
select-package:
+ flutter: false
flutter_analyze:
run: melos exec --depends-on="graphql" -c 1 -- "flutter format --set-exit-if-changed . && flutter analyze . --fatal-infos"
description: Run dart analyzer in a specific package.
select-package:
- scope: "graphql_flutter"
+ flutter: true
client_analyze:
run: melos exec -c 1 -- "dart format --set-exit-if-changed . && dart analyze . --fatal-infos"
description: Run dart analyzer in a specific package.
select-package:
- scope: "graphql"
+ flutter: false
test:
description: Run tests in a specific package.
- run: melos exec --concurrency=2 -- "dart pub get && dart pub run test"
+ run: melos exec -- "dart pub get && dart pub run test"
select-package:
+ flutter: false
dir-exists:
- "test/"
env:
@@ -35,9 +37,9 @@ scripts:
flutter_test:
description: Run tests in a specific package.
- run: melos exec --depends-on="graphql" --concurrency=2 -- "flutter test"
+ run: melos exec --depends-on="graphql" -- "flutter test"
select-package:
- scope: "graphql_flutter"
+ flutter: true
dir-exists:
- "test/"
env:
@@ -45,9 +47,9 @@ scripts:
client_test:
description: Run tests in a specific package.
- run: melos exec --concurrency=2 -- "dart pub get && dart pub run test"
+ run: melos exec -- "dart pub get && dart pub run test"
select-package:
- scope: "graphql"
+ flutter: false
dir-exists:
- "test/"
env:
@@ -55,9 +57,9 @@ scripts:
flutter_test_coverage:
description: Run tests in a specific package.
- run: melos exec --depends-on="graphql" --concurrency=2 -- "flutter test --coverage"
+ run: melos exec --depends-on="graphql" -- "flutter test --coverage"
select-package:
- scope: "graphql_flutter"
+ flutter: true
dir-exists:
- "test/"
env:
@@ -65,12 +67,16 @@ scripts:
client_test_coverage:
description: Run tests in a specific package.
- run: melos exec --concurrency=2 -- "dart run test --coverage="coverage" && dart run coverage:format_coverage --lcov --in=coverage --out=coverage.lcov --packages=.dart_tool/package_config.json --report-on=lib"
+ run: melos exec -- "dart run test --coverage="coverage" && dart run coverage:format_coverage --lcov --in=coverage --out=coverage.lcov --packages=.dart_tool/package_config.json --report-on=lib"
select-package:
- scope: "graphql"
+ flutter: false
dir-exists:
- "test/"
env:
MELOS_TEST: true
format: dart format -o write .
+
+command:
+ bootstrap:
+ usePubspecOverrides: true

diff:
@@ -9,7 +9,6 @@ import 'package:gql/language.dart';
import 'package:graphql/client.dart';
import './helpers.dart';
-import './mock_server/ws_echo_server.dart';
import 'mock_server/ws_echo_server.dart';
SocketClient getTestClient({

diff:
-// This is a basic Flutter widget test.
-// To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter
-// provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to
-// find child widgets in the widget tree, read text, and verify that the values of widget properties
-// are correct.
-
-// import 'package:flutter/material.dart';
-// import 'package:flutter_test/flutter_test.dart';
-
-void main() {
- // TODO: write tests
-}

original_message: feat: Refactor GH actions


message: investigate issue

diff:
@@ -154,6 +154,333 @@ void main() {
equals('bar'),
);
});
+ test('issue 1208', () async {
+ final client = GraphQLClient(
+ cache: GraphQLCache(
+ possibleTypes: {
+ 'WalletContentBlock': {'WalletContentBlockList'},
+ 'WalletContentItem': {'WalletContentItemContentPreview'}
+ },
+ ),
+ link: link,
+ );
+ const data = {
+ "__typename": "Query",
+ "walletGetContent": {
+ "__typename": "WalletGetContentPayload",
+ "blocks": [
+ {
+ "__typename": "WalletContentBlockList",
+ "id": "1",
+ "blockType": {
+ "__typename": "WalletContentBlockType",
+ "id": "horizontal-scrolling-list",
+ },
+ "caption": "Featured",
+ "items": [
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "1",
+ "caption": "Remarkable Women #4740",
+ "imageURL":
+ "https://lh3.googleusercontent.com/1kXncBpOzPjZAC46GDNXvW4NL0zDv1e_E6vzsTtKEAOAvb0SwbWzwljDY5hulMqRA4zQytjYjV7B-a_jYtNG6zy9dKssov9RRRLu5ss=w600",
+ "link":
+ "https://opensea.io/assets/ethereum/0x3e69baab7a742c83499661c5db92386b2424df11/4740"
+ },
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "2",
+ "caption": "Inftspaces",
+ "imageURL":
+ "https://lh3.googleusercontent.com/8h6WQ4_Kxn0c6rBujSe0flZMTSKiWGWiMOSNaSTPUchakVq4TmyBOWJi4E0GU3_5b7v4jF-vIPR24uziltXMfQHgzEj3zBCkW_Tbsw=s0",
+ "link":
+ "https://opensea.io/assets/ethereum/0xb11288c7237765db4782dfddcc49e31bb80132b8/90"
+ },
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "3",
+ "caption": "Love on Mars",
+ "imageURL":
+ "https://lh3.googleusercontent.com/XoOJwf3DQ6ZWMpmmLkVqfZD3AqEM3G0xQCXNur0-u5-HAHpoMHyPJQL__5H0wpJsy2cafaaCSSxI5YPjnzCZ-4ME7hnJXrgd5292nA=s0",
+ "link":
+ "https://opensea.io/collection/love-on-mars-by-ruben-rojas-x-omgdrops"
+ }
+ ]
+ },
+ {
+ "__typename": "WalletContentBlockList",
+ "id": "2",
+ "blockType": {
+ "__typename": "WalletContentBlockType",
+ "id": "vertical-short-list"
+ },
+ "caption": "New",
+ "items": [
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "4",
+ "caption": "MSO LAB Genesis #844",
+ "imageURL":
+ "https://lh3.googleusercontent.com/FCcSKSsYaNk9-bzks7ZWIEjFPiL8xrtb8aYnO2DG0piF9Eja1AkY55Yan-KQqtpcwISdpTWNz_W0yrmmmQM2hAHNG_AIXl7E4ATG2Q=w600",
+ "link":
+ "https://opensea.io/assets/ethereum/0xc2ac394984f3850027dac95fe8a62e446c5fb786/844"
+ },
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "5",
+ "caption": "The Outsiders Genesis",
+ "imageURL":
+ "https://lh3.googleusercontent.com/AM8EkaQl32uR8N1RIiJa5ogVG1gh2faX6u4a1TAa_NhsGkA2CRG0pFfMOOxcc09q2KFq5cQNljTFkDbbScWpUT70es83Di0RRWD7fjM=s0",
+ "link":
+ "https://opensea.io/collection/the-outsiders-genesis"
+ },
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "6",
+ "caption": "Bored Ape Yacht Club #6386",
+ "imageURL":
+ "https://img.seadn.io/files/968a4be8ada4a55b59de561d15522b4e.png?fit=max&w=600",
+ "link":
+ "https://opensea.io/assets/ethereum/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/6386"
+ }
+ ]
+ }
+ ]
+ }
+ };
+ final _options = QueryOptions(
+ document: parseString(r'''
+query WalletGetContent($input: WalletGetContentInput!) {
+ __typename
+ walletGetContent(input: $input) {
+ __typename
+ blocks {
+ __typename
+ ... on WalletContentBlockList {
+ __typename
+ id
+ blockType {
+ __typename
+ id
+ }
+ caption
+ items {
+ __typename
+ ... on WalletContentItemContentPreview {
+ __typename
+ id
+ caption
+ imageURL
+ link
+ }
+ }
+ }
+ }
+ }
+}
+'''),
+ variables: <String, dynamic>{
+ 'input': {'id': 'foo'},
+ },
+ );
+
+ when(
+ link.request(any),
+ ).thenAnswer(
+ (_) => Stream.fromIterable([
+ Response(
+ data: data,
+ context: Context().withEntry(
+ HttpLinkResponseContext(
+ statusCode: 200,
+ headers: {'foo': 'bar'},
+ ),
+ ),
+ response: {},
+ ),
+ ]),
+ );
+
+ final QueryResult r = await client.query(_options);
+ expect(r.exception, isNull);
+ expect(r.data, equals(data));
+ });
+ test('issue 1208, duplicate IDs', () async {
+ final client = GraphQLClient(
+ cache: GraphQLCache(
+ possibleTypes: {
+ 'WalletContentBlock': {'WalletContentBlockList'},
+ 'WalletContentItem': {'WalletContentItemContentPreview'}
+ },
+ ),
+ link: link,
+ );
+ const data = {
+ "__typename": "Query",
+ "walletGetContent": {
+ "__typename": "WalletGetContentPayload",
+ "blocks": [
+ {
+ "__typename": "WalletContentBlockList",
+ "id": "1",
+ "blockType": {
+ "__typename": "WalletContentBlockType",
+ "id": "horizontal-scrolling-list",
+ },
+ "caption": "Featured",
+ "items": [
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "1",
+ "caption": "Remarkable Women #4740",
+ "imageURL":
+ "https://lh3.googleusercontent.com/1kXncBpOzPjZAC46GDNXvW4NL0zDv1e_E6vzsTtKEAOAvb0SwbWzwljDY5hulMqRA4zQytjYjV7B-a_jYtNG6zy9dKssov9RRRLu5ss=w600",
+ "link":
+ "https://opensea.io/assets/ethereum/0x3e69baab7a742c83499661c5db92386b2424df11/4740"
+ },
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "2",
+ "caption": "Inftspaces",
+ "imageURL":
+ "https://lh3.googleusercontent.com/8h6WQ4_Kxn0c6rBujSe0flZMTSKiWGWiMOSNaSTPUchakVq4TmyBOWJi4E0GU3_5b7v4jF-vIPR24uziltXMfQHgzEj3zBCkW_Tbsw=s0",
+ "link":
+ "https://opensea.io/assets/ethereum/0xb11288c7237765db4782dfddcc49e31bb80132b8/90"
+ },
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "3",
+ "caption": "Love on Mars",
+ "imageURL":
+ "https://lh3.googleusercontent.com/XoOJwf3DQ6ZWMpmmLkVqfZD3AqEM3G0xQCXNur0-u5-HAHpoMHyPJQL__5H0wpJsy2cafaaCSSxI5YPjnzCZ-4ME7hnJXrgd5292nA=s0",
+ "link":
+ "https://opensea.io/collection/love-on-mars-by-ruben-rojas-x-omgdrops"
+ }
+ ]
+ },
+ {
+ "__typename": "WalletContentBlockList",
+ "id": "2",
+ "blockType": {
+ "__typename": "WalletContentBlockType",
+ "id": "vertical-short-list"
+ },
+ "caption": "New",
+ "items": [
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "1",
+ "caption": "MSO LAB Genesis #844",
+ "imageURL":
+ "https://lh3.googleusercontent.com/FCcSKSsYaNk9-bzks7ZWIEjFPiL8xrtb8aYnO2DG0piF9Eja1AkY55Yan-KQqtpcwISdpTWNz_W0yrmmmQM2hAHNG_AIXl7E4ATG2Q=w600",
+ "link":
+ "https://opensea.io/assets/ethereum/0xc2ac394984f3850027dac95fe8a62e446c5fb786/844"
+ },
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "2",
+ "caption": "The Outsiders Genesis",
+ "imageURL":
+ "https://lh3.googleusercontent.com/AM8EkaQl32uR8N1RIiJa5ogVG1gh2faX6u4a1TAa_NhsGkA2CRG0pFfMOOxcc09q2KFq5cQNljTFkDbbScWpUT70es83Di0RRWD7fjM=s0",
+ "link":
+ "https://opensea.io/collection/the-outsiders-genesis"
+ },
+ {
+ "__typename": "WalletContentItemContentPreview",
+ "id": "3",
+ "caption": "Bored Ape Yacht Club #6386",
+ "imageURL":
+ "https://img.seadn.io/files/968a4be8ada4a55b59de561d15522b4e.png?fit=max&w=600",
+ "link":
+ "https://opensea.io/assets/ethereum/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/6386"
+ }
+ ]
+ }
+ ]
+ }
+ };
+ final _options = QueryOptions(
+ document: parseString(r'''
+query WalletGetContent($input: WalletGetContentInput!) {
+ __typename
+ walletGetContent(input: $input) {
+ __typename
+ blocks {
+ __typename
+ ... on WalletContentBlockList {
+ __typename
+ id
+ blockType {
+ __typename
+ id
+ }
+ caption
+ items {
+ __typename
+ ... on WalletContentItemContentPreview {
+ __typename
+ id
+ caption
+ imageURL
+ link
+ }
+ }
+ }
+ }
+ }
+}
+'''),
+ variables: <String, dynamic>{
+ 'input': {'id': 'foo'},
+ },
+ );
+
+ when(
+ link.request(any),
+ ).thenAnswer(
+ (_) => Stream.fromIterable([
+ Response(
+ data: data,
+ context: Context().withEntry(
+ HttpLinkResponseContext(
+ statusCode: 200,
+ headers: {'foo': 'bar'},
+ ),
+ ),
+ response: {},
+ ),
+ ]),
+ );
+
+ final QueryResult r = await client.query(_options);
+ expect(r.exception, isNull);
+ expect(
+ r.data,
+ equals({
+ ...data,
+ "walletGetContent": {
+ ...data["walletGetContent"] as Map<String, dynamic>,
+ "blocks": [
+ {
+ ...(data["walletGetContent"]
+ as Map<String, dynamic>)["blocks"][0]
+ as Map<String, dynamic>,
+ "items": ((data["walletGetContent"]
+ as Map<String, dynamic>)["blocks"][1]
+ as Map<String, dynamic>)["items"],
+ },
+ {
+ ...(data["walletGetContent"]
+ as Map<String, dynamic>)["blocks"][1]
+ as Map<String, dynamic>,
+ "items": ((data["walletGetContent"]
+ as Map<String, dynamic>)["blocks"][1]
+ as Map<String, dynamic>)["items"],
+ }
+ ]
+ }
+ }));
+ });
test('successful response with parser', () async {
final ResultParserFn<List<String>> parserFn = (data) {

original_message: investigate issue #1208


message: issue 38 - Dragging item in treeview past end of branch crashes

diff:
@@ -516,7 +516,8 @@ public class TreeView extends JInternalFrame implements iSubscriber {
// do not allow drop past end of branch
int childIndex = dropLocation.getChildIndex();
int parentIndex = parentNode.getChildCount();
- if (parentIndex == childIndex) return false;
+
+ if (childIndex+1 >= parentIndex) return false;
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
TreePath dropPath = new TreePath(selectedNode.getPath());

repo: impulseadventure/guislice