diff --git a/normalize/.vscode/launch.json b/normalize/.vscode/launch.json new file mode 100644 index 00000000..0908a5f9 --- /dev/null +++ b/normalize/.vscode/launch.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/normalize/lib/src/config/normalization_config.dart b/normalize/lib/src/config/normalization_config.dart index 3aedca70..30b142cb 100644 --- a/normalize/lib/src/config/normalization_config.dart +++ b/normalize/lib/src/config/normalization_config.dart @@ -26,6 +26,8 @@ class NormalizationConfig { /// Whether to accept or return partial data. final bool allowPartialData; + final Map> possibleTypeOf; + NormalizationConfig({ required this.read, required this.variables, @@ -35,5 +37,6 @@ class NormalizationConfig { required this.dataIdFromObject, required this.addTypename, required this.allowPartialData, + required this.possibleTypeOf, }); } diff --git a/normalize/lib/src/denormalize_fragment.dart b/normalize/lib/src/denormalize_fragment.dart index b172eef1..8d9306e6 100644 --- a/normalize/lib/src/denormalize_fragment.dart +++ b/normalize/lib/src/denormalize_fragment.dart @@ -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. @@ -36,6 +34,7 @@ Map? denormalizeFragment({ bool returnPartialData = false, bool handleException = true, String referenceKey = '\$ref', + Map> possibleTypeOf = const {}, }) { if (addTypename) { document = transform( @@ -86,6 +85,7 @@ Map? denormalizeFragment({ dataIdFromObject: dataIdFromObject, addTypename: addTypename, allowPartialData: returnPartialData, + possibleTypeOf: possibleTypeOf, ); try { diff --git a/normalize/lib/src/denormalize_node.dart b/normalize/lib/src/denormalize_node.dart index a61d85df..161d2f8a 100644 --- a/normalize/lib/src/denormalize_node.dart +++ b/normalize/lib/src/denormalize_node.dart @@ -45,6 +45,7 @@ Object? denormalizeNode({ typename: typename, selectionSet: selectionSet, fragmentMap: config.fragmentMap, + possibleTypeOf: config.possibleTypeOf, ); final result = subNodes.fold>( diff --git a/normalize/lib/src/denormalize_operation.dart b/normalize/lib/src/denormalize_operation.dart index 5bb7a3a4..cc682e9c 100644 --- a/normalize/lib/src/denormalize_operation.dart +++ b/normalize/lib/src/denormalize_operation.dart @@ -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'; @@ -31,6 +29,7 @@ Map? denormalizeOperation({ bool returnPartialData = false, bool handleException = true, String referenceKey = '\$ref', + Map> possibleTypeOf = const {}, }) { if (addTypename) { document = transform( @@ -55,6 +54,7 @@ Map? denormalizeOperation({ dataIdFromObject: dataIdFromObject, addTypename: addTypename, allowPartialData: returnPartialData, + possibleTypeOf: possibleTypeOf, ); try { diff --git a/normalize/lib/src/normalize_fragment.dart b/normalize/lib/src/normalize_fragment.dart index c84bd387..03410293 100644 --- a/normalize/lib/src/normalize_fragment.dart +++ b/normalize/lib/src/normalize_fragment.dart @@ -39,6 +39,7 @@ void normalizeFragment({ bool addTypename = false, String referenceKey = '\$ref', bool acceptPartialData = true, + Map> possibleTypeOf = const {}, }) { // Always add typenames to ensure data is stored with typename document = transform( @@ -75,6 +76,7 @@ void normalizeFragment({ addTypename: addTypename, dataIdFromObject: dataIdFromObject, allowPartialData: acceptPartialData, + possibleTypeOf: possibleTypeOf, ); final dataId = resolveDataId( diff --git a/normalize/lib/src/normalize_node.dart b/normalize/lib/src/normalize_node.dart index e368177f..0db6ca06 100644 --- a/normalize/lib/src/normalize_node.dart +++ b/normalize/lib/src/normalize_node.dart @@ -52,6 +52,7 @@ Object? normalizeNode({ typename: typename, selectionSet: selectionSet, fragmentMap: config.fragmentMap, + possibleTypeOf: config.possibleTypeOf, ); final dataToMerge = { diff --git a/normalize/lib/src/normalize_operation.dart b/normalize/lib/src/normalize_operation.dart index 3cae735a..f40023bf 100644 --- a/normalize/lib/src/normalize_operation.dart +++ b/normalize/lib/src/normalize_operation.dart @@ -35,6 +35,7 @@ void normalizeOperation({ bool addTypename = false, bool acceptPartialData = true, String referenceKey = '\$ref', + Map> possibleTypeOf = const {}, }) { if (addTypename) { document = transform( @@ -59,6 +60,7 @@ void normalizeOperation({ addTypename: addTypename, dataIdFromObject: dataIdFromObject, allowPartialData: acceptPartialData, + possibleTypeOf: possibleTypeOf, ); write( diff --git a/normalize/lib/src/policies/field_policy.dart b/normalize/lib/src/policies/field_policy.dart index 493f72a7..63d923f3 100644 --- a/normalize/lib/src/policies/field_policy.dart +++ b/normalize/lib/src/policies/field_policy.dart @@ -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); @@ -50,6 +50,7 @@ class FieldFunctionOptions { dataIdFromObject: _config.dataIdFromObject, addTypename: _config.addTypename, allowPartialData: true, + possibleTypeOf: _config.possibleTypeOf, ), ) as T?; } diff --git a/normalize/lib/src/utils/expand_fragments.dart b/normalize/lib/src/utils/expand_fragments.dart index 300566af..f8ee5b9a 100644 --- a/normalize/lib/src/utils/expand_fragments.dart +++ b/normalize/lib/src/utils/expand_fragments.dart @@ -6,39 +6,42 @@ List expandFragments({ required String? typename, required SelectionSetNode selectionSet, required Map fragmentMap, + required Map> possibleTypeOf, }) { final fieldNodes = []; 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)); diff --git a/normalize/lib/src/utils/operation_field_names.dart b/normalize/lib/src/utils/operation_field_names.dart index a763aa73..7e121d4a 100644 --- a/normalize/lib/src/utils/operation_field_names.dart +++ b/normalize/lib/src/utils/operation_field_names.dart @@ -13,6 +13,7 @@ List operationFieldNames( String operationName, Map vars, Map typePolicies, + Map> possibleTypeOf, ) { final operationDefinition = getOperationDefinition( document, @@ -27,6 +28,7 @@ List operationFieldNames( typename: rootTypename, selectionSet: operationDefinition.selectionSet, fragmentMap: fragmentMap, + possibleTypeOf: possibleTypeOf, ); final typePolicy = typePolicies[rootTypename]; return fields.map((fieldNode) { diff --git a/normalize/test/fragment_spread.dart b/normalize/test/fragment_spread.dart new file mode 100644 index 00000000..fb936e03 --- /dev/null +++ b/normalize/test/fragment_spread.dart @@ -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), + ); + }); + }); +}