@@ -14,12 +14,23 @@ import type {
14
14
import { Graph } from 'graphlib' ;
15
15
import {
16
16
isSpecifiedScalarType ,
17
+ Kind ,
17
18
visit ,
18
19
} from 'graphql' ;
19
20
20
- export const isListType = ( typ ?: TypeNode ) : typ is ListTypeNode => typ ?. kind === 'ListType' ;
21
- export const isNonNullType = ( typ ?: TypeNode ) : typ is NonNullTypeNode => typ ?. kind === 'NonNullType' ;
22
- export const isNamedType = ( typ ?: TypeNode ) : typ is NamedTypeNode => typ ?. kind === 'NamedType' ;
21
+ /**
22
+ * Recursively unwraps a GraphQL type until it reaches the NamedType.
23
+ *
24
+ * Since a GraphQL type is defined as either a NamedTypeNode, ListTypeNode, or NonNullTypeNode,
25
+ * this implementation safely recurses until the underlying NamedTypeNode is reached.
26
+ */
27
+ function getNamedType ( typeNode : TypeNode ) : NamedTypeNode {
28
+ return typeNode . kind === Kind . NAMED_TYPE ? typeNode : getNamedType ( typeNode . type ) ;
29
+ }
30
+
31
+ export const isListType = ( typ ?: TypeNode ) : typ is ListTypeNode => typ ?. kind === Kind . LIST_TYPE ;
32
+ export const isNonNullType = ( typ ?: TypeNode ) : typ is NonNullTypeNode => typ ?. kind === Kind . NON_NULL_TYPE ;
33
+ export const isNamedType = ( typ ?: TypeNode ) : typ is NamedTypeNode => typ ?. kind === Kind . NAMED_TYPE ;
23
34
24
35
export const isInput = ( kind : string ) => kind . includes ( 'Input' ) ;
25
36
@@ -48,44 +59,50 @@ export function InterfaceTypeDefinitionBuilder(useInterfaceTypes: boolean | unde
48
59
export function topologicalSortAST ( schema : GraphQLSchema , ast : DocumentNode ) : DocumentNode {
49
60
const dependencyGraph = new Graph ( ) ;
50
61
const targetKinds = [
51
- 'ObjectTypeDefinition' ,
52
- 'InputObjectTypeDefinition' ,
53
- 'EnumTypeDefinition' ,
54
- 'UnionTypeDefinition' ,
55
- 'ScalarTypeDefinition' ,
62
+ Kind . OBJECT_TYPE_DEFINITION ,
63
+ Kind . INPUT_OBJECT_TYPE_DEFINITION ,
64
+ Kind . INTERFACE_TYPE_DEFINITION ,
65
+ Kind . SCALAR_TYPE_DEFINITION ,
66
+ Kind . ENUM_TYPE_DEFINITION ,
67
+ Kind . UNION_TYPE_DEFINITION ,
56
68
] ;
57
69
58
70
visit < DocumentNode > ( ast , {
59
71
enter : ( node ) => {
60
72
switch ( node . kind ) {
61
- case 'ObjectTypeDefinition' :
62
- case 'InputObjectTypeDefinition' : {
73
+ case Kind . OBJECT_TYPE_DEFINITION :
74
+ case Kind . INPUT_OBJECT_TYPE_DEFINITION :
75
+ case Kind . INTERFACE_TYPE_DEFINITION : {
63
76
const typeName = node . name . value ;
64
77
dependencyGraph . setNode ( typeName ) ;
65
78
66
79
if ( node . fields ) {
67
80
node . fields . forEach ( ( field ) => {
68
- if ( field . type . kind === 'NamedType' ) {
69
- const dependency = field . type . name . value ;
70
- const typ = schema . getType ( dependency ) ;
71
- if ( typ ?. astNode ?. kind === undefined || ! targetKinds . includes ( typ . astNode . kind ) )
72
- return ;
73
-
74
- if ( ! dependencyGraph . hasNode ( dependency ) )
75
- dependencyGraph . setNode ( dependency ) ;
81
+ // Unwrap the type
82
+ const namedTypeNode = getNamedType ( field . type ) ;
83
+ const dependency = namedTypeNode . name . value ;
84
+ const namedType = schema . getType ( dependency ) ;
85
+ if (
86
+ namedType ?. astNode ?. kind === undefined
87
+ || ! targetKinds . includes ( namedType . astNode . kind )
88
+ ) {
89
+ return ;
90
+ }
76
91
77
- dependencyGraph . setEdge ( typeName , dependency ) ;
92
+ if ( ! dependencyGraph . hasNode ( dependency ) ) {
93
+ dependencyGraph . setNode ( dependency ) ;
78
94
}
95
+ dependencyGraph . setEdge ( typeName , dependency ) ;
79
96
} ) ;
80
97
}
81
98
break ;
82
99
}
83
- case 'ScalarTypeDefinition' :
84
- case 'EnumTypeDefinition' : {
100
+ case Kind . SCALAR_TYPE_DEFINITION :
101
+ case Kind . ENUM_TYPE_DEFINITION : {
85
102
dependencyGraph . setNode ( node . name . value ) ;
86
103
break ;
87
104
}
88
- case 'UnionTypeDefinition' : {
105
+ case Kind . UNION_TYPE_DEFINITION : {
89
106
const dependency = node . name . value ;
90
107
if ( ! dependencyGraph . hasNode ( dependency ) )
91
108
dependencyGraph . setNode ( dependency ) ;
@@ -111,16 +128,16 @@ export function topologicalSortAST(schema: GraphQLSchema, ast: DocumentNode): Do
111
128
// Create a map of definitions for quick access, using the definition's name as the key.
112
129
const definitionsMap : Map < string , DefinitionNode > = new Map ( ) ;
113
130
114
- // SCHEMA_DEFINITION does not have name.
131
+ // SCHEMA_DEFINITION does not have a name.
115
132
// https://spec.graphql.org/October2021/#sec-Schema
116
- const astDefinitions = ast . definitions . filter ( def => def . kind !== 'SchemaDefinition' ) ;
133
+ const astDefinitions = ast . definitions . filter ( def => def . kind !== Kind . SCHEMA_DEFINITION ) ;
117
134
118
135
astDefinitions . forEach ( ( definition ) => {
119
136
if ( hasNameField ( definition ) && definition . name )
120
137
definitionsMap . set ( definition . name . value , definition ) ;
121
138
} ) ;
122
139
123
- // Two arrays to store sorted and not sorted definitions.
140
+ // Two arrays to store sorted and unsorted definitions.
124
141
const sortedDefinitions : DefinitionNode [ ] = [ ] ;
125
142
const notSortedDefinitions : DefinitionNode [ ] = [ ] ;
126
143
@@ -141,7 +158,7 @@ export function topologicalSortAST(schema: GraphQLSchema, ast: DocumentNode): Do
141
158
142
159
if ( newDefinitions . length !== astDefinitions . length ) {
143
160
throw new Error (
144
- `unexpected definition length after sorted : want ${ astDefinitions . length } but got ${ newDefinitions . length } ` ,
161
+ `Unexpected definition length after sorting : want ${ astDefinitions . length } but got ${ newDefinitions . length } ` ,
145
162
) ;
146
163
}
147
164
@@ -155,33 +172,35 @@ function hasNameField(node: ASTNode): node is DefinitionNode & { name?: NameNode
155
172
return 'name' in node ;
156
173
}
157
174
158
- // Re-implemented w/o CycleException version
159
- // https://github.com/dagrejs/graphlib/blob/8d27cb89029081c72eb89dde652602805bdd0a34/lib/alg/topsort.js
175
+ /**
176
+ * [Re-implemented topsort function][topsort-ref] with cycle handling. This version iterates over
177
+ * all nodes in the graph to ensure every node is visited, even if the graph contains cycles.
178
+ *
179
+ * [topsort-ref]: https://github.com/dagrejs/graphlib/blob/8d27cb89029081c72eb89dde652602805bdd0a34/lib/alg/topsort.js
180
+ */
160
181
export function topsort ( g : Graph ) : string [ ] {
161
- const visited : Record < string , boolean > = { } ;
162
- const stack : Record < string , boolean > = { } ;
163
- const results : any [ ] = [ ] ;
182
+ const visited = new Set < string > ( ) ;
183
+ const results : Array < string > = [ ] ;
164
184
165
185
function visit ( node : string ) {
166
- if ( ! ( node in visited ) ) {
167
- stack [ node ] = true ;
168
- visited [ node ] = true ;
169
- const predecessors = g . predecessors ( node ) ;
170
- if ( Array . isArray ( predecessors ) )
171
- predecessors . forEach ( node => visit ( node ) ) ;
172
-
173
- delete stack [ node ] ;
174
- results . push ( node ) ;
175
- }
186
+ if ( visited . has ( node ) )
187
+ return ;
188
+ visited . add ( node ) ;
189
+ // Recursively visit all predecessors of the node.
190
+ g . predecessors ( node ) ?. forEach ( visit ) ;
191
+ results . push ( node ) ;
176
192
}
177
193
178
- g . sinks ( ) . forEach ( node => visit ( node ) ) ;
194
+ // Visit every node in the graph (instead of only sinks)
195
+ g . nodes ( ) . forEach ( visit ) ;
179
196
180
197
return results . reverse ( ) ;
181
198
}
182
199
183
200
export function isGeneratedByIntrospection ( schema : GraphQLSchema ) : boolean {
184
- return Object . entries ( schema . getTypeMap ( ) ) . filter ( ( [ name , type ] ) => ! name . startsWith ( '__' ) && ! isSpecifiedScalarType ( type ) ) . every ( ( [ , type ] ) => type . astNode === undefined )
201
+ return Object . entries ( schema . getTypeMap ( ) )
202
+ . filter ( ( [ name , type ] ) => ! name . startsWith ( '__' ) && ! isSpecifiedScalarType ( type ) )
203
+ . every ( ( [ , type ] ) => type . astNode === undefined )
185
204
}
186
205
187
206
// https://spec.graphql.org/October2021/#EscapedCharacter
0 commit comments