Skip to content

Commit

Permalink
feat(client): partialDataPolicy for configuring rejections
Browse files Browse the repository at this point in the history
  • Loading branch information
micimize committed Nov 6, 2020
1 parent a0a967f commit 0a7cd28
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 18 deletions.
12 changes: 8 additions & 4 deletions packages/graphql/lib/src/cache/_normalizing_data_proxy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,19 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
///
/// Passed through to normalize. When [denormalizeOperation] isn't passed [returnPartialData],
/// It will simply return `null` if any part of the query can't be constructed.
///
/// **NOTE**: This is not exposed as a configuration for a reason.
/// If enabled, it would be easy to eagerly return an unexpected partial result from the cache,
/// resulting in mangled and hard-to-reason-about app state.
@protected
bool get returnPartialData => false;

/// Used for testing.
/// Whether it is permissible to write partial data to the this proxy.
/// Determined by [PartialDataCachePolicy]
///
/// Passed through to normalize. When [normalizeOperation] isn't passed [acceptPartialData],
/// It will simply return `null` if any part of the query can't be constructed.
@protected
bool get acceptPartialData => false;
/// It will set missing fields to `null` if any part of a structurally valid query result is missing.
bool get acceptPartialData;

/// Flag used to request a (re)broadcast from the [QueryManager].
///
Expand Down
11 changes: 10 additions & 1 deletion packages/graphql/lib/src/cache/_optimistic_transactions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import 'package:meta/meta.dart';
import 'package:graphql/src/cache/_normalizing_data_proxy.dart';
import 'package:graphql/src/cache/data_proxy.dart';

import 'package:graphql/src/cache/cache.dart' show GraphQLCache;
import 'package:graphql/src/cache/cache.dart'
show GraphQLCache, PartialDataCachePolicy;

/// API for users to provide cache updates through
typedef CacheTransaction = GraphQLDataProxy Function(GraphQLDataProxy proxy);
Expand All @@ -32,6 +33,14 @@ class OptimisticProxy extends NormalizingDataProxy {

GraphQLCache cache;

/// Whether it is permissible to write partial data, as determined by [partialDataPolicy].
///
/// Passed through to normalize. When [normalizeOperation] isn't passed [acceptPartialData],
/// It will set missing fields to `null` if any part of a structurally valid query result is missing.
@override
bool get acceptPartialData =>
cache.partialDataPolicy != PartialDataCachePolicy.reject;

/// `typePolicies` to pass down to `normalize` (proxied from [cache])
get typePolicies => cache.typePolicies;

Expand Down
42 changes: 36 additions & 6 deletions packages/graphql/lib/src/cache/cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@ class GraphQLCache extends NormalizingDataProxy {
Store store,
this.dataIdFromObject,
this.typePolicies = const {},

/// Input variable sanitizer for referencing custom scalar types in cache keys.
///
/// Defaults to [sanitizeFilesForCache]. Can be set to `null` to disable sanitization.
/// If present, a sanitizer will be built with [variableSanitizer]
this.partialDataPolicy = PartialDataCachePolicy.acceptForOptimisticData,
Object Function(Object) sanitizeVariables = sanitizeFilesForCache,
}) : sanitizeVariables = variableSanitizer(sanitizeVariables),
store = store ?? InMemoryStore();
Expand All @@ -46,12 +42,30 @@ class GraphQLCache extends NormalizingDataProxy {
/// rebroadcast operations.
final Store store;

/// Determines how partial data should be handled when written to the cache
///
/// [acceptForOptimisticData] is the default and recommended, as it provides
/// the best balance between safety and usability.
final PartialDataCachePolicy partialDataPolicy;

/// Whether it is permissible to write partial data, as determined by [partialDataPolicy].
///
/// Passed through to normalize. When [normalizeOperation] isn't passed [acceptPartialData],
/// It will simply return `null` if any part of the query can't be constructed.
@override
bool get acceptPartialData =>
partialDataPolicy == PartialDataCachePolicy.accept;

/// `typePolicies` to pass down to [normalize]
final Map<String, TypePolicy> typePolicies;

/// Optional `dataIdFromObject` function to pass through to [normalize]
final DataIdResolver dataIdFromObject;

/// Input variable sanitizer for referencing custom scalar types in cache keys.
///
/// Defaults to [sanitizeFilesForCache]. Can be set to `null` to disable sanitization.
/// If present, a sanitizer will be built with [variableSanitizer]
@override
final SanitizeVariables sanitizeVariables;

Expand Down Expand Up @@ -116,7 +130,7 @@ class GraphQLCache extends NormalizingDataProxy {
/// Write normalized data into the cache,
/// deeply merging maps with existing values
///
/// Called from [writeQuery] and [writeFragment].
/// Called from [witeQuery] and [writeFragment].
void writeNormalized(String dataId, dynamic value) {
if (value is Map<String, Object>) {
final existing = store.get(dataId);
Expand Down Expand Up @@ -190,3 +204,19 @@ class GraphQLCache extends NormalizingDataProxy {
broadcastRequested = true;
}
}

/// Determines how partial data should be handled when written to the cache
///
/// [acceptForOptimisticData] is the default and recommended, as it provides
/// the best balance between safety and usability.
enum PartialDataCachePolicy {
/// Accept partial data in all cases (default).
accept,

/// Accept partial data, but only for transient optimistic writes made with
/// [GraphQLCache.recordOptimisticTransaction]
acceptForOptimisticData,

/// Reject partial data in all cases.
reject,
}
6 changes: 3 additions & 3 deletions packages/graphql/lib/src/core/observable_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class ObservableQuery {
/// as refetching from the `cache` does not make sense.
Future<QueryResult> refetch() {
if (isRefetchSafe) {
addResult(QueryResult.loading(data: latestResult.data));
addResult(QueryResult.loading(data: latestResult?.data));
return queryManager.refetchQuery(queryId);
}
throw Exception('Query is not refetch safe');
Expand Down Expand Up @@ -213,7 +213,7 @@ class ObservableQuery {
Future<QueryResult> fetchMore(FetchMoreOptions fetchMoreOptions) async {
assert(fetchMoreOptions.updateQuery != null);

addResult(QueryResult.loading(data: latestResult.data));
addResult(QueryResult.loading(data: latestResult?.data));

return fetchMoreImplementation(
fetchMoreOptions,
Expand All @@ -239,7 +239,7 @@ class ObservableQuery {
}

if (options.carryForwardDataOnException && result.hasException) {
result.data ??= latestResult.data;
result.data ??= latestResult?.data;
}

if (lifecycle == QueryLifecycle.pending && result.isConcrete) {
Expand Down
10 changes: 7 additions & 3 deletions packages/graphql/lib/src/core/query_result.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,17 @@ final _eagerSources = {
QueryResultSource.optimisticResult
};

/// A single operation result
class QueryResult {
QueryResult({
this.data,
this.exception,
@required this.source,
}) : timestamp = DateTime.now(),
assert(source != null);
}) : timestamp = DateTime.now();

/// An empty result. Can be used as a placeholder when an operation
/// has not been executed yet.
factory QueryResult.empty() => QueryResult(source: null);

factory QueryResult.loading({
Map<String, dynamic> data,
Expand All @@ -86,7 +90,7 @@ class QueryResult {

/// The source of the result data.
///
/// null when unexecuted.
/// `null` when unexecuted.
/// Will be set when encountering an error during any execution attempt
QueryResultSource source;

Expand Down
5 changes: 4 additions & 1 deletion packages/graphql/test/cache/graphql_cache_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,10 @@ void main() {

GraphQLCache cache;
setUp(() {
cache = GraphQLCache(dataIdFromObject: customDataIdFromObject);
cache = GraphQLCache(
dataIdFromObject: customDataIdFromObject,
partialDataPolicy: PartialDataCachePolicy.reject,
);
});

test('.writeQuery .readQuery round trip', () {
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql/test/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ overridePrint(testFn(List<String> log)) => () {

class TestCache extends GraphQLCache {
bool get returnPartialData => debuggingUnexpectedTestFailures;

get partialDataPolicy => PartialDataCachePolicy.reject;
}

GraphQLCache getTestCache() => TestCache();

0 comments on commit 0a7cd28

Please sign in to comment.