Skip to content

Commit

Permalink
feat: Add support for better fragment type resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
budde377 committed May 30, 2021
1 parent 43bb04c commit b759691
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 21 deletions.
25 changes: 25 additions & 0 deletions normalize/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Dart: Run all Tests",
"type": "dart",
"request": "launch",
"program": "./test/"
},
{
"name": "Dart: Run Tests",
"type": "dart",
"request": "launch",
"program": "./test/fragment_spread.dart"
},
{
"name": "normalize",
"request": "launch",
"type": "dart"
}
]
}
3 changes: 3 additions & 0 deletions normalize/lib/src/config/normalization_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class NormalizationConfig {
/// Whether to accept or return partial data.
final bool allowPartialData;

final Map<String, Set<String>> possibleTypeOf;

NormalizationConfig({
required this.read,
required this.variables,
Expand All @@ -35,5 +37,6 @@ class NormalizationConfig {
required this.dataIdFromObject,
required this.addTypename,
required this.allowPartialData,
required this.possibleTypeOf,
});
}
4 changes: 2 additions & 2 deletions normalize/lib/src/denormalize_fragment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import 'package:gql/ast.dart';
import 'package:normalize/normalize.dart';

import 'package:normalize/src/config/normalization_config.dart';
import 'package:normalize/src/policies/type_policy.dart';
import 'package:normalize/src/utils/get_fragment_map.dart';
import 'package:normalize/src/utils/resolve_data_id.dart';
import 'package:normalize/src/utils/add_typename_visitor.dart';
import 'package:normalize/src/utils/exceptions.dart';
import 'package:normalize/src/denormalize_node.dart';

/// Denormalizes data for a given fragment.
Expand Down Expand Up @@ -36,6 +34,7 @@ Map<String, dynamic>? denormalizeFragment({
bool returnPartialData = false,
bool handleException = true,
String referenceKey = '\$ref',
Map<String, Set<String>> possibleTypeOf = const {},
}) {
if (addTypename) {
document = transform(
Expand Down Expand Up @@ -86,6 +85,7 @@ Map<String, dynamic>? denormalizeFragment({
dataIdFromObject: dataIdFromObject,
addTypename: addTypename,
allowPartialData: returnPartialData,
possibleTypeOf: possibleTypeOf,
);

try {
Expand Down
1 change: 1 addition & 0 deletions normalize/lib/src/denormalize_node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Object? denormalizeNode({
typename: typename,
selectionSet: selectionSet,
fragmentMap: config.fragmentMap,
possibleTypeOf: config.possibleTypeOf,
);

final result = subNodes.fold<Map<String, dynamic>>(
Expand Down
4 changes: 2 additions & 2 deletions normalize/lib/src/denormalize_operation.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import 'package:gql/ast.dart';
import 'package:normalize/normalize.dart';

import 'package:normalize/src/policies/type_policy.dart';
import 'package:normalize/src/utils/resolve_root_typename.dart';
import 'package:normalize/src/utils/add_typename_visitor.dart';
import 'package:normalize/src/utils/exceptions.dart';
import 'package:normalize/src/utils/get_operation_definition.dart';
import 'package:normalize/src/denormalize_node.dart';
import 'package:normalize/src/config/normalization_config.dart';
Expand All @@ -31,6 +29,7 @@ Map<String, dynamic>? denormalizeOperation({
bool returnPartialData = false,
bool handleException = true,
String referenceKey = '\$ref',
Map<String, Set<String>> possibleTypeOf = const {},
}) {
if (addTypename) {
document = transform(
Expand All @@ -55,6 +54,7 @@ Map<String, dynamic>? denormalizeOperation({
dataIdFromObject: dataIdFromObject,
addTypename: addTypename,
allowPartialData: returnPartialData,
possibleTypeOf: possibleTypeOf,
);

try {
Expand Down
2 changes: 2 additions & 0 deletions normalize/lib/src/normalize_fragment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ void normalizeFragment({
bool addTypename = false,
String referenceKey = '\$ref',
bool acceptPartialData = true,
Map<String, Set<String>> possibleTypeOf = const {},
}) {
// Always add typenames to ensure data is stored with typename
document = transform(
Expand Down Expand Up @@ -75,6 +76,7 @@ void normalizeFragment({
addTypename: addTypename,
dataIdFromObject: dataIdFromObject,
allowPartialData: acceptPartialData,
possibleTypeOf: possibleTypeOf,
);

final dataId = resolveDataId(
Expand Down
1 change: 1 addition & 0 deletions normalize/lib/src/normalize_node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Object? normalizeNode({
typename: typename,
selectionSet: selectionSet,
fragmentMap: config.fragmentMap,
possibleTypeOf: config.possibleTypeOf,
);

final dataToMerge = <String, dynamic>{
Expand Down
2 changes: 2 additions & 0 deletions normalize/lib/src/normalize_operation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ void normalizeOperation({
bool addTypename = false,
bool acceptPartialData = true,
String referenceKey = '\$ref',
Map<String, Set<String>> possibleTypeOf = const {},
}) {
if (addTypename) {
document = transform(
Expand All @@ -59,6 +60,7 @@ void normalizeOperation({
addTypename: addTypename,
dataIdFromObject: dataIdFromObject,
allowPartialData: acceptPartialData,
possibleTypeOf: possibleTypeOf,
);

write(
Expand Down
3 changes: 2 additions & 1 deletion normalize/lib/src/policies/field_policy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FieldFunctionOptions {
FieldFunctionOptions({
required this.field,
required NormalizationConfig config,
}) : _config = config,
}) : _config = config,
variables = config.variables,
args = argsWithValues(config.variables, field.arguments);

Expand Down Expand Up @@ -50,6 +50,7 @@ class FieldFunctionOptions {
dataIdFromObject: _config.dataIdFromObject,
addTypename: _config.addTypename,
allowPartialData: true,
possibleTypeOf: _config.possibleTypeOf,
),
) as T?;
}
Expand Down
35 changes: 19 additions & 16 deletions normalize/lib/src/utils/expand_fragments.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,42 @@ List<FieldNode> expandFragments({
required String? typename,
required SelectionSetNode selectionSet,
required Map<String, FragmentDefinitionNode> fragmentMap,
required Map<String, Set<String>> possibleTypeOf,
}) {
final fieldNodes = <FieldNode>[];

for (var selectionNode in selectionSet.selections) {
if (selectionNode is FieldNode) {
fieldNodes.add(selectionNode);
} else if (selectionNode is InlineFragmentNode) {
// Only include this fragment if the type name matches
if (selectionNode.typeCondition?.on.name.value == typename) {
fieldNodes.addAll(
expandFragments(
typename: typename,
selectionSet: selectionNode.selectionSet,
fragmentMap: fragmentMap,
),
);
}
continue;
}
String? fragmentOnName;
SelectionSetNode fragmentSelectionSet;
if (selectionNode is InlineFragmentNode) {
fragmentOnName = selectionNode.typeCondition?.on.name.value ?? typename;
fragmentSelectionSet = selectionNode.selectionSet;
} else if (selectionNode is FragmentSpreadNode) {
final fragment = fragmentMap[selectionNode.name.value];

if (fragment == null) {
throw Exception('Missing fragment ${selectionNode.name.value}');
}

fragmentOnName = fragment.typeCondition.on.name.value;
fragmentSelectionSet = fragment.selectionSet;
} else {
throw (FormatException('Unknown selection node type'));
}
if (typename == null ||
fragmentOnName == null ||
fragmentOnName == typename ||
possibleTypeOf[typename]?.contains(fragmentOnName) == true) {
fieldNodes.addAll(
expandFragments(
typename: typename,
selectionSet: fragment.selectionSet,
selectionSet: fragmentSelectionSet,
fragmentMap: fragmentMap,
possibleTypeOf: possibleTypeOf,
),
);
} else {
throw (FormatException('Unknown selection node type'));
}
}
return List.from(_mergeSelections(fieldNodes));
Expand Down
2 changes: 2 additions & 0 deletions normalize/lib/src/utils/operation_field_names.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ List<String> operationFieldNames<TData, TVars>(
String operationName,
Map<String, dynamic> vars,
Map<String, TypePolicy> typePolicies,
Map<String, Set<String>> possibleTypeOf,
) {
final operationDefinition = getOperationDefinition(
document,
Expand All @@ -27,6 +28,7 @@ List<String> operationFieldNames<TData, TVars>(
typename: rootTypename,
selectionSet: operationDefinition.selectionSet,
fragmentMap: fragmentMap,
possibleTypeOf: possibleTypeOf,
);
final typePolicy = typePolicies[rootTypename];
return fields.map((fieldNode) {
Expand Down
95 changes: 95 additions & 0 deletions normalize/test/fragment_spread.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:test/test.dart';
import 'package:gql/language.dart';

import 'package:normalize/normalize.dart';

void main() {
group('Normalizing and denormalizing fragments', () {
test('Simple fragment', () {
final possibleTypeOf = {
'Author': {'User'},
'Audience': {'User'},
};
final document = parseString('''
fragment FAuthor on Author {
__typename
id
}
fragment FAudience on Audience {
__typename
id
numHands
isClapping
}
fragment FUser on User {
id
__typename
name
}
query {
users {
...FAuthor
...FAudience
...FUser
}
}
''');
final data = {
'users': [
{'__typename': 'Author', 'id': '1', 'name': 'Knud'},
{
'__typename': 'Audience',
'id': 'a',
'name': 'Lars',
'numHands': 2,
'isClapping': false
},
],
};

final normalizedMap = {
'Author:1': {
'__typename': 'Author',
'id': '1',
'name': 'Knud',
},
'Audience:a': {
'__typename': 'Audience',
'id': 'a',
'numHands': 2,
'isClapping': false,
'name': 'Lars'
},
'Query': {
'users': [
{r'$ref': 'Author:1'},
{r'$ref': 'Audience:a'}
]
},
};
final normalizedResult = {};
normalizeOperation(
read: (dataId) => normalizedResult[dataId],
write: (dataId, value) => normalizedResult[dataId] = value,
document: document,
data: data,
acceptPartialData: false,
possibleTypeOf: possibleTypeOf,
);
expect(
normalizedResult,
equals(normalizedMap),
);

expect(
denormalizeOperation(
document: document,
handleException: false,
read: (dataId) => normalizedMap[dataId],
possibleTypeOf: possibleTypeOf,
),
equals(data),
);
});
});
}

0 comments on commit b759691

Please sign in to comment.