Skip to content

Commit

Permalink
refactor(client): Fragment and FragmentRequest for more normalized api
Browse files Browse the repository at this point in the history
  • Loading branch information
micimize committed Sep 25, 2020
1 parent 3e06854 commit 2f04058
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 81 deletions.
42 changes: 20 additions & 22 deletions packages/graphql/lib/src/cache/_normalizing_data_proxy.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import 'package:graphql/src/cache/fragment.dart';
import "package:meta/meta.dart";

import 'package:gql_exec/gql_exec.dart' show Request;
import 'package:gql/ast.dart' show DocumentNode;

import 'package:normalize/normalize.dart';

import './data_proxy.dart';
Expand Down Expand Up @@ -75,23 +74,22 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
returnPartialData: returnPartialData,
);

Map<String, dynamic> readFragment({
@required DocumentNode fragment,
@required Map<String, dynamic> idFields,
String fragmentName,
Map<String, dynamic> variables,
Map<String, dynamic> readFragment(
FragmentRequest fragmentRequest, {
bool optimistic = true,
}) =>
denormalizeFragment(
// provided from cache
reader: (dataId) => readNormalized(dataId, optimistic: optimistic),
fragment: fragment,
idFields: idFields,
fragmentName: fragmentName,
variables: sanitizeVariables(variables),
typePolicies: typePolicies,
addTypename: addTypename ?? false,
dataIdFromObject: dataIdFromObject,
returnPartialData: returnPartialData,
addTypename: addTypename ?? false,
// provided from request
fragment: fragmentRequest.fragment.document,
idFields: fragmentRequest.idFields,
fragmentName: fragmentRequest.fragment.fragmentName,
variables: sanitizeVariables(fragmentRequest.variables),
);

void writeQuery(
Expand All @@ -113,23 +111,23 @@ abstract class NormalizingDataProxy extends GraphQLDataProxy {
}
}

void writeFragment({
@required DocumentNode fragment,
@required Map<String, dynamic> idFields,
void writeFragment(
FragmentRequest request, {
@required Map<String, dynamic> data,
String fragmentName,
Map<String, dynamic> variables,
bool broadcast = true,
}) {
normalizeFragment(
// provided from cache
writer: (dataId, value) => writeNormalized(dataId, value),
fragment: fragment,
idFields: idFields,
data: data,
fragmentName: fragmentName,
variables: sanitizeVariables(variables),
typePolicies: typePolicies,
dataIdFromObject: dataIdFromObject,
// provided from request
fragment: request.fragment.document,
idFields: request.idFields,
fragmentName: request.fragment.fragmentName,
variables: sanitizeVariables(request.variables),
// data
data: data,
);
if (broadcast ?? true) {
broadcastRequested = true;
Expand Down
10 changes: 9 additions & 1 deletion packages/graphql/lib/src/cache/cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@ import 'package:normalize/normalize.dart';
export 'package:graphql/src/cache/data_proxy.dart';
export 'package:graphql/src/cache/store.dart';
export 'package:graphql/src/cache/hive_store.dart';
export 'package:graphql/src/cache/fragment.dart';

typedef VariableEncoder = Object Function(Object t);

/// Optimmistic GraphQL Entity cache with [normalize] [TypePolicy] support
/// Optimistic GraphQL Entity cache with [normalize] [TypePolicy] support
/// and configurable [store].
///
/// **NOTE**: The default [InMemoryStore] does _not_ persist to disk.
/// The recommended store for persistent environments is the [HiveStore].
///
/// [dataIdFromObject] and [typePolicies] are passed down to [normalize] operations, which say:
/// > IDs are determined by the following:
/// >
/// > 1. If a `TypePolicy` is provided for the given type, it's `TypePolicy.keyFields` are used.
/// > 2. If a `dataIdFromObject` funciton is provided, the result is used.
/// > 3. The `id` or `_id` field (respectively) are used.
class GraphQLCache extends NormalizingDataProxy {
GraphQLCache({
Store store,
Expand Down
24 changes: 11 additions & 13 deletions packages/graphql/lib/src/cache/data_proxy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import "package:meta/meta.dart";
import 'package:gql_exec/gql_exec.dart' show Request;
import 'package:gql/ast.dart' show DocumentNode;

import './fragment.dart';

/// A proxy to the normalized data living in our store.
///
/// This interface allows a user to read and write
Expand All @@ -16,12 +18,10 @@ abstract class GraphQLDataProxy {
/// Reads a GraphQL fragment from any arbitrary id.
///
/// If there is more than one fragment in the provided document
/// then a `fragmentName` must be provided to select the correct fragment.
Map<String, dynamic> readFragment({
@required DocumentNode fragment,
@required Map<String, dynamic> idFields,
String fragmentName,
Map<String, dynamic> variables,
/// then a `fragmentName` must be provided to `fragmentRequest.fragment`
/// to select the correct fragment.
Map<String, dynamic> readFragment(
FragmentRequest fragmentRequest, {
bool optimistic,
});

Expand All @@ -42,13 +42,11 @@ abstract class GraphQLDataProxy {
/// then [broadcast] changes to watchers unless `broadcast: false`
///
/// If there is more than one fragment in the provided document
/// then a `fragmentName` must be provided to select the correct fragment.
void writeFragment({
@required DocumentNode fragment,
@required Map<String, dynamic> idFields,
@required Map<String, dynamic> data,
String fragmentName,
Map<String, dynamic> variables,
/// then a `fragmentName` must be provided to `fragmentRequest.fragment`
/// to select the correct fragment.
void writeFragment(
FragmentRequest fragmentRequest, {
Map<String, dynamic> data,
bool broadcast,
});
}
113 changes: 113 additions & 0 deletions packages/graphql/lib/src/cache/fragment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import "package:collection/collection.dart";
import "package:gql/ast.dart";
import 'package:graphql/client.dart';
import "package:meta/meta.dart";

/// A fragment in a [document], optionally defined by [fragmentName]
@immutable
class Fragment {
/// Document containing at least one [FragmentDefinitionNode]
final DocumentNode document;

/// Name of the fragment definition
///
/// Must be specified if [document] contains more than one [FragmentDefinitionNode]
final String fragmentName;

const Fragment({
@required this.document,
this.fragmentName,
}) : assert(document != null);

List<Object> _getChildren() => [
document,
fragmentName,
];

@override
bool operator ==(Object o) =>
identical(this, o) ||
(o is Fragment &&
const ListEquality<Object>(
DeepCollectionEquality(),
).equals(
o._getChildren(),
_getChildren(),
));

@override
int get hashCode => const ListEquality<Object>(
DeepCollectionEquality(),
).hash(
_getChildren(),
);

@override
String toString() =>
"Fragment(document: $document, fragmentName: $fragmentName)";

/// helper for building a [FragmentRequest]
@experimental
FragmentRequest asRequest({
@required Map<String, dynamic> idFields,
Map<String, dynamic> variables = const <String, dynamic>{},
}) =>
FragmentRequest(fragment: this, idFields: idFields, variables: variables);
}

/// Cache access request of [fragment] with [variables].
@immutable
class FragmentRequest {
/// [Fragment] to be read or written
final Fragment fragment;

/// Variables of the fragment for this request
final Map<String, dynamic> variables;

/// Map which includes all identifying data (usually `{__typename, id }`)
final Map<String, dynamic> idFields;

const FragmentRequest({
@required this.fragment,
@required this.idFields,
this.variables = const <String, dynamic>{},
}) : assert(fragment != null),
assert(idFields != null);

List<Object> _getChildren() => [
fragment,
variables,
idFields,
];

@override
bool operator ==(Object o) =>
identical(this, o) ||
(o is FragmentRequest &&
const ListEquality<Object>(
DeepCollectionEquality(),
).equals(
o._getChildren(),
_getChildren(),
));

@override
int get hashCode => const ListEquality<Object>(
DeepCollectionEquality(),
).hash(
_getChildren(),
);

@override
String toString() =>
"FragmentRequest(fragment: $fragment, variables: $variables)";
}

extension OperationRequestHelper on Operation {
/// helper for building a [Request]
@experimental
Request asRequest({
Map<String, dynamic> variables = const <String, dynamic>{},
}) =>
Request(operation: this, variables: variables);
}
28 changes: 8 additions & 20 deletions packages/graphql/lib/src/graphql_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -204,18 +204,12 @@ class GraphQLClient implements GraphQLDataProxy {
cache.readQuery(request, optimistic: optimistic);

/// pass through to [cache.readFragment]
readFragment({
@required fragment,
@required idFields,
fragmentName,
variables,
readFragment(
fragmentRequest, {
optimistic,
}) =>
cache.readFragment(
fragment: fragment,
idFields: idFields,
fragmentName: fragmentName,
variables: variables,
fragmentRequest,
optimistic: optimistic,
);

Expand All @@ -226,21 +220,15 @@ class GraphQLClient implements GraphQLDataProxy {
}

/// pass through to [cache.writeFragment] and then rebroadcast any changes.
void writeFragment({
@required fragment,
@required idFields,
@required data,
fragmentName,
variables,
void writeFragment(
fragmentRequest, {
broadcast,
data,
}) {
cache.writeFragment(
fragment: fragment,
idFields: idFields,
data: data,
fragmentName: fragmentName,
variables: variables,
fragmentRequest,
broadcast: broadcast,
data: data,
);
queryManager.maybeRebroadcastQueries();
}
Expand Down
39 changes: 24 additions & 15 deletions packages/graphql/test/cache/cache_data.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:gql_exec/gql_exec.dart';
import 'package:gql/language.dart';
import 'package:graphql/client.dart' show Fragment;
import 'package:graphql/src/utilities/helpers.dart';
import 'package:meta/meta.dart';
import 'package:http/http.dart' as http;
Expand Down Expand Up @@ -136,22 +137,30 @@ final originalCValue = <String, dynamic>{
'id': 6,
'cField': 'value',
};
final originalCFragment = parseString(r'''
fragment partialC on C {
__typename
id
cField
}
''');
final originalCFragment = Fragment(
document: parseString(
r'''
fragment partialC on C {
__typename
id
cField
}
''',
),
);

final updatedCFragment = parseString(r'''
fragment partialC on C {
__typename
id
new
cField
}
''');
final updatedCFragment = Fragment(
document: parseString(
r'''
fragment partialC on C {
__typename
id
new
cField
}
''',
),
);

final updatedCValue = <String, dynamic>{
'__typename': 'C',
Expand Down
Loading

0 comments on commit 2f04058

Please sign in to comment.