diff --git a/codegen/gql_code_builder/lib/data.dart b/codegen/gql_code_builder/lib/data.dart index dff7f16b..6639c125 100644 --- a/codegen/gql_code_builder/lib/data.dart +++ b/codegen/gql_code_builder/lib/data.dart @@ -4,6 +4,7 @@ import "package:gql/ast.dart"; import "package:gql_code_builder/src/common.dart"; import "package:gql_code_builder/src/config/data_class_config.dart"; import "package:gql_code_builder/src/config/when_extension_config.dart"; +import "package:gql_code_builder/src/utils/possible_types.dart"; import "./source.dart"; import "./src/operation/data.dart"; @@ -26,8 +27,11 @@ Library buildDataLibrary( ), ]) { final fragmentMap = _fragmentMap(docSource); + final possibleTypesMap = dataClassConfig.reuseFragments + ? _possibleTypesMap(schemaSource) + : >{}; final dataClassAliasMap = dataClassConfig.reuseFragments - ? _dataClassAliasMap(docSource, fragmentMap) + ? _dataClassAliasMap(docSource, fragmentMap, possibleTypesMap) : {}; final operationDataClasses = docSource.document.definitions @@ -40,6 +44,7 @@ Library buildDataLibrary( typeOverrides, whenExtensionConfig, fragmentMap, + possibleTypesMap, dataClassAliasMap, ), ) @@ -55,6 +60,7 @@ Library buildDataLibrary( typeOverrides, whenExtensionConfig, fragmentMap, + possibleTypesMap, dataClassAliasMap, ), ) @@ -80,16 +86,40 @@ Map _fragmentMap(SourceNode source) => { for (final import in source.imports) ..._fragmentMap(import) }; +Map> _possibleTypesMap(SourceNode source, + [Set? visitedSource, Map>? possibleTypesMap]) { + visitedSource ??= {}; + possibleTypesMap ??= {}; + + source.imports.forEach((s) { + if (!visitedSource!.contains(source.url)) { + visitedSource.add(source.url); + _possibleTypesMap(s, visitedSource, possibleTypesMap); + } + }); + + source.document.possibleTypesMap().forEach((key, value) { + possibleTypesMap![key] ??= {}; + possibleTypesMap[key]!.addAll(value); + }); + + return possibleTypesMap; +} + Map _dataClassAliasMap( - SourceNode source, Map fragmentMap, - [Map? aliasMap, Set? visitedSource]) { + SourceNode source, + Map fragmentMap, + Map> possibleTypesMap, + [Map? aliasMap, + Set? visitedSource]) { aliasMap ??= {}; visitedSource ??= {}; source.imports.forEach((s) { if (!visitedSource!.contains(source.url)) { visitedSource.add(source.url); - _dataClassAliasMap(s, fragmentMap, aliasMap); + _dataClassAliasMap( + s, fragmentMap, possibleTypesMap, aliasMap, visitedSource); } }); @@ -101,6 +131,7 @@ Map _dataClassAliasMap( selections: def.selectionSet.selections, fragmentMap: fragmentMap, aliasMap: aliasMap, + possibleTypesMap: possibleTypesMap, ); } @@ -112,6 +143,7 @@ Map _dataClassAliasMap( selections: def.selectionSet.selections, fragmentMap: fragmentMap, aliasMap: aliasMap, + possibleTypesMap: possibleTypesMap, ); _dataClassAliasMapDFS( typeRefPrefix: builtClassName("${def.name.value}Data"), @@ -119,6 +151,7 @@ Map _dataClassAliasMap( selections: def.selectionSet.selections, fragmentMap: fragmentMap, aliasMap: aliasMap, + possibleTypesMap: possibleTypesMap, ); } @@ -131,17 +164,20 @@ void _dataClassAliasMapDFS({ required List selections, required Map fragmentMap, required Map aliasMap, + required Map> possibleTypesMap, + bool shouldAlwaysGenerate = false, }) { if (selections.isEmpty) return; - // flatten selections to extract untouched fragments while visiting children. + // shrink selections to extract untouched fragments while visiting children. final shrunkenSelections = shrinkSelections(mergeSelections(selections, fragmentMap), fragmentMap); // alias single fragment and finish final selectionsWithoutTypename = shrunkenSelections .where((s) => !(s is FieldNode && s.name.value == "__typename")); - if (selectionsWithoutTypename.length == 1 && + if (!shouldAlwaysGenerate && + selectionsWithoutTypename.length == 1 && selectionsWithoutTypename.first is FragmentSpreadNode) { final node = selectionsWithoutTypename.first as FragmentSpreadNode; final fragment = fragmentMap[node.name.value]; @@ -149,7 +185,6 @@ void _dataClassAliasMapDFS({ aliasMap[typeRefPrefix] = refer(fragmentTypeName, "${fragment!.url ?? ""}#data"); // print("alias $typeRefPrefix => $fragmentTypeName"); - return; } for (final node in selectionsWithoutTypename) { @@ -179,20 +214,36 @@ void _dataClassAliasMapDFS({ selections: exclusiveFragmentSelections, fragmentMap: fragmentMap, aliasMap: aliasMap, + possibleTypesMap: possibleTypesMap, ); } else if (node is InlineFragmentNode) { + /// TODO: Handle inline fragments without a type condition if (node.typeCondition != null) { - /// TODO: Handle inline fragments without a type condition + final currentType = node.typeCondition!.on.name.value; + final interfaces = possibleTypesMap.entries + .where((e) => e.value.contains(currentType)) + .map((e) => e.key) + .toSet(); + + final selectionsIncludingInterfaces = [ + ...selections.where((s) => !(s is InlineFragmentNode)), + // spread all interfaces which current type is belongs to + ...selections + .whereType() + .where((s) => + s == node || + interfaces.contains(s.typeCondition?.on.name.value)) + .expand((s) => s.selectionSet.selections), + ]; + _dataClassAliasMapDFS( - typeRefPrefix: - "${typeRefPrefix}__as${node.typeCondition!.on.name.value}", + typeRefPrefix: "${typeRefPrefix}__as${currentType}", getAliasTypeName: getAliasTypeName, - selections: [ - ...selections.where((s) => s != node), - ...node.selectionSet.selections, - ], + selections: selectionsIncludingInterfaces, fragmentMap: fragmentMap, aliasMap: aliasMap, + possibleTypesMap: possibleTypesMap, + shouldAlwaysGenerate: true, ); } } else if (node is FieldNode && node.selectionSet != null) { @@ -202,6 +253,7 @@ void _dataClassAliasMapDFS({ selections: node.selectionSet!.selections, fragmentMap: fragmentMap, aliasMap: aliasMap, + possibleTypesMap: possibleTypesMap, ); } } diff --git a/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart b/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart index 7b168ca6..4a409b18 100644 --- a/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart +++ b/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart @@ -22,6 +22,7 @@ List buildInlineFragmentClasses({ required String type, required Map typeOverrides, required Map fragmentMap, + required Map> possibleTypesMap, required Map dataClassAliasMap, required Map superclassSelections, required List inlineFragments, @@ -32,6 +33,7 @@ List buildInlineFragmentClasses({ baseTypeName: name, inlineFragments: inlineFragments, config: whenExtensionConfig, + possibleTypesMap: possibleTypesMap, dataClassAliasMap: dataClassAliasMap, ); return [ @@ -71,6 +73,7 @@ List buildInlineFragmentClasses({ fragmentMap, ), fragmentMap: fragmentMap, + possibleTypesMap: possibleTypesMap, dataClassAliasMap: dataClassAliasMap, schemaSource: schemaSource, type: type, @@ -94,28 +97,35 @@ List buildInlineFragmentClasses({ // print("alias $typeName => ${dataClassAliasMap[typeName]!.symbol}"); return false; } + // is it okay to inlcude interfaces? return true; }).expand( (inlineFragment) => buildSelectionSetDataClasses( - name: "${name}__as${inlineFragment.typeCondition!.on.name.value}", - selections: mergeSelections( - [ - ...selections.whereType(), - ...selections.whereType(), - ...inlineFragment.selectionSet.selections, - ], - fragmentMap, - ), - fragmentMap: fragmentMap, - dataClassAliasMap: dataClassAliasMap, - schemaSource: schemaSource, - type: inlineFragment.typeCondition!.on.name.value, - typeOverrides: typeOverrides, - superclassSelections: { - name: SourceSelections(url: null, selections: selections) - }, - built: built, - whenExtensionConfig: whenExtensionConfig), + name: "${name}__as${inlineFragment.typeCondition!.on.name.value}", + selections: mergeSelections( + [ + ...selections.whereType(), + ...selections.whereType(), + ...inlineFragment.selectionSet.selections, + ], + fragmentMap, + ), + fragmentMap: fragmentMap, + possibleTypesMap: possibleTypesMap, + dataClassAliasMap: dataClassAliasMap, + schemaSource: schemaSource, + type: inlineFragment.typeCondition!.on.name.value, + typeOverrides: typeOverrides, + superclassSelections: { + name: SourceSelections(url: null, selections: selections), + if (dataClassAliasMap.isNotEmpty) + ...Map.fromEntries(superclassSelections.entries.map((e) => MapEntry( + "${e.key}__as${inlineFragment.typeCondition!.on.name.value}", + e.value))), + }, + built: built, + whenExtensionConfig: whenExtensionConfig, + ), ), ]; } diff --git a/codegen/gql_code_builder/lib/src/operation/data.dart b/codegen/gql_code_builder/lib/src/operation/data.dart index e8fea5ab..4f56ea76 100644 --- a/codegen/gql_code_builder/lib/src/operation/data.dart +++ b/codegen/gql_code_builder/lib/src/operation/data.dart @@ -15,6 +15,7 @@ List buildOperationDataClasses( Map typeOverrides, InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig, Map fragmentMap, + Map> possibleTypesMap, Map dataClassAliasMap, ) { if (op.name == null) { @@ -34,6 +35,7 @@ List buildOperationDataClasses( ), typeOverrides: typeOverrides, fragmentMap: fragmentMap, + possibleTypesMap: possibleTypesMap, dataClassAliasMap: dataClassAliasMap, superclassSelections: {}, whenExtensionConfig: whenExtensionConfig, @@ -47,6 +49,7 @@ List buildFragmentDataClasses( Map typeOverrides, InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig, Map fragmentMap, + Map> possibleTypesMap, Map dataClassAliasMap, ) { final selections = mergeSelections( @@ -62,6 +65,7 @@ List buildFragmentDataClasses( type: frag.typeCondition.on.name.value, typeOverrides: typeOverrides, fragmentMap: fragmentMap, + possibleTypesMap: possibleTypesMap, dataClassAliasMap: dataClassAliasMap, superclassSelections: {}, built: false, @@ -75,6 +79,7 @@ List buildFragmentDataClasses( type: frag.typeCondition.on.name.value, typeOverrides: typeOverrides, fragmentMap: fragmentMap, + possibleTypesMap: possibleTypesMap, dataClassAliasMap: dataClassAliasMap, superclassSelections: { frag.name.value: SourceSelections( @@ -120,6 +125,7 @@ List buildSelectionSetDataClasses({ required String type, required Map typeOverrides, required Map fragmentMap, + required Map> possibleTypesMap, required Map dataClassAliasMap, required Map superclassSelections, bool built = true, @@ -180,6 +186,7 @@ List buildSelectionSetDataClasses({ type: type, typeOverrides: typeOverrides, fragmentMap: fragmentMap, + possibleTypesMap: possibleTypesMap, dataClassAliasMap: dataClassAliasMap, superclassSelections: superclassSelections, inlineFragments: inlineFragments, @@ -236,6 +243,7 @@ List buildSelectionSetDataClasses({ name: "${name}_${field.alias?.value ?? field.name.value}", selections: field.selectionSet!.selections, fragmentMap: fragmentMap, + possibleTypesMap: possibleTypesMap, dataClassAliasMap: dataClassAliasMap, schemaSource: schemaSource, type: unwrapTypeNode( @@ -292,20 +300,32 @@ List shrinkSelections( } } + final duplicateIndices = {}; for (final node in unmerged.whereType().toList()) { + if (!fragmentMap.containsKey(node.name.value)) { + throw "Cannot find a fragment ${node.name.value} from current file."; + } final fragment = fragmentMap[node.name.value]!; - final spreadIndex = unmerged.indexOf(node); - final duplicateIndexList = []; + final fragmentExpanded = { + ...fragment.selections, + ..._expandFragmentSpreads(fragment.selections, fragmentMap) + }; + final fragmentSpreadIndex = unmerged.indexOf(node); unmerged.forEachIndexed((selectionIndex, selection) { - if (selectionIndex > spreadIndex && - fragment.selections.any((s) => s.hashCode == selection.hashCode)) { - duplicateIndexList.add(selectionIndex); + if (selectionIndex != fragmentSpreadIndex && + !(selection is FieldNode && selection.name.value == "__typename") && + fragmentExpanded.any((s) => s.hashCode == selection.hashCode)) { + duplicateIndices.add(selectionIndex); } }); - duplicateIndexList.reversed.forEach(unmerged.removeAt); } - return unmerged; + return unmerged + .asMap() + .entries + .where((e) => !duplicateIndices.contains(e.key)) + .map((e) => e.value) + .toList(); } /// Deeply merges field nodes diff --git a/codegen/gql_code_builder/lib/src/when_extension.dart b/codegen/gql_code_builder/lib/src/when_extension.dart index 197bcab4..55894ece 100644 --- a/codegen/gql_code_builder/lib/src/when_extension.dart +++ b/codegen/gql_code_builder/lib/src/when_extension.dart @@ -40,9 +40,15 @@ Extension? inlineFragmentWhenExtension( {required String baseTypeName, required List inlineFragments, required InlineFragmentSpreadWhenExtensionConfig config, + required Map> possibleTypesMap, required Map dataClassAliasMap}) { final inlineFragmentsWithTypConditions = inlineFragments - .where((inlineFragment) => inlineFragment.typeCondition != null) + .where((inlineFragment) => + inlineFragment.typeCondition != null + // except interfaces on when extension + && + !possibleTypesMap + .containsKey(inlineFragment.typeCondition!.on.name.value)) .toList(); final nothingToDo = inlineFragmentsWithTypConditions.isEmpty || @@ -187,8 +193,7 @@ Extension? inlineFragmentWhenExtension( [ _switchTypeName, _curlyBracketOpen, - ...inlineFragments - .where((element) => element.typeCondition != null) + ...inlineFragmentsWithTypConditions .map((inlineFragment) => Block.of([ _caseStatement(inlineFragment), maybeWhenCaseBody(inlineFragment),