@@ -93,6 +93,14 @@ function getRootTypeNamesFromDocumentNode(document: DocumentNode) {
93
93
return names ;
94
94
}
95
95
96
+ type ObjectLikeNode =
97
+ | ObjectTypeExtensionNode
98
+ | ObjectTypeDefinitionNode
99
+ | InterfaceTypeDefinitionNode
100
+ | InterfaceTypeExtensionNode
101
+ | InputObjectTypeDefinitionNode
102
+ | InputObjectTypeExtensionNode ;
103
+
96
104
/**
97
105
* Takes a subgraph document node and a set of tag filters and transforms the document node to contain `@inaccessible` directives on all fields not included by the applied filter.
98
106
* Note: you probably want to use `filterSubgraphs` instead, as it also applies the correct post step required after applying this.
@@ -102,8 +110,11 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema(
102
110
filter : Federation2SubgraphDocumentNodeByTagsFilter ,
103
111
) : {
104
112
typeDefs : DocumentNode ;
113
+ /** Types within THIS subgraph where all fields are inaccessible */
105
114
typesWithAllFieldsInaccessible : Map < string , boolean > ;
106
115
transformTagDirectives : ReturnType < typeof createTransformTagDirectives > ;
116
+ /** Types in this subgraph that have the inaccessible directive applied */
117
+ typesWithInaccessibleApplied : Set < string > ;
107
118
} {
108
119
const { resolveImportName } = extractLinkImplementations ( documentNode ) ;
109
120
const inaccessibleDirectiveName = resolveImportName (
@@ -149,71 +160,119 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema(
149
160
} ;
150
161
}
151
162
152
- function fieldLikeObjectHandler (
153
- node :
154
- | ObjectTypeExtensionNode
155
- | ObjectTypeDefinitionNode
156
- | InterfaceTypeDefinitionNode
157
- | InterfaceTypeExtensionNode
158
- | InputObjectTypeDefinitionNode
159
- | InputObjectTypeExtensionNode ,
160
- ) {
161
- const tagsOnNode = getTagsOnNode ( node ) ;
162
-
163
- let isAllFieldsInaccessible = true ;
164
-
165
- const newNode = {
166
- ...node ,
167
- fields : node . fields ?. map ( node => {
168
- const tagsOnNode = getTagsOnNode ( node ) ;
169
-
170
- if ( node . kind === Kind . FIELD_DEFINITION ) {
171
- node = {
172
- ...node ,
173
- arguments : node . arguments ?. map ( fieldArgumentHandler ) ,
174
- } as FieldDefinitionNode ;
163
+ const definitionsBySchemaCoordinate = new Map < string , Array < ObjectLikeNode > > ( ) ;
164
+
165
+ //
166
+ // A type can be defined multiple times within a subgraph and we need to find all implementations
167
+ // for determining whether the full type, or only some fields are part of the public contract schema
168
+ //
169
+ for ( const definition of documentNode . definitions ) {
170
+ switch ( definition . kind ) {
171
+ case Kind . OBJECT_TYPE_DEFINITION :
172
+ case Kind . OBJECT_TYPE_EXTENSION :
173
+ case Kind . INTERFACE_TYPE_DEFINITION :
174
+ case Kind . INTERFACE_TYPE_EXTENSION :
175
+ case Kind . INPUT_OBJECT_TYPE_DEFINITION :
176
+ case Kind . INPUT_OBJECT_TYPE_EXTENSION : {
177
+ let items = definitionsBySchemaCoordinate . get ( definition . name . value ) ;
178
+ if ( ! items ) {
179
+ items = [ ] ;
180
+ definitionsBySchemaCoordinate . set ( definition . name . value , items ) ;
175
181
}
182
+ items . push ( definition ) ;
183
+ }
184
+ }
185
+ }
186
+
187
+ // Tracking for which type already has `@inaccessible` applied (can only occur once)
188
+ const typesWithInaccessibleApplied = new Set < string > ( ) ;
189
+ // These are later used within the visitor to actually replace the nodes.
190
+ const replacementTypeNodes = new Map < ObjectLikeNode , ObjectLikeNode > ( ) ;
191
+
192
+ for ( const [ typeName , nodes ] of definitionsBySchemaCoordinate ) {
193
+ /** After processing all nodes implementing a type, we know whether all or only some fields are inaccessible */
194
+ let isSomeFieldsAccessible = false ;
195
+ /** First node occurance record as stored within the `replacementTypeNodes` map. */
196
+ let firstReplacementTypeNodeRecord : {
197
+ key : ObjectLikeNode ;
198
+ value : ObjectLikeNode ;
199
+ } | null = null ;
200
+
201
+ for ( const node of nodes ) {
202
+ const tagsOnNode = getTagsOnNode ( node ) ;
203
+ let newNode = {
204
+ ...node ,
205
+ fields : node . fields ?. map ( node => {
206
+ const tagsOnNode = getTagsOnNode ( node ) ;
207
+
208
+ if ( node . kind === Kind . FIELD_DEFINITION ) {
209
+ node = {
210
+ ...node ,
211
+ arguments : node . arguments ?. map ( fieldArgumentHandler ) ,
212
+ } as FieldDefinitionNode ;
213
+ }
214
+
215
+ if (
216
+ ( filter . include . size && ! hasIntersection ( tagsOnNode , filter . include ) ) ||
217
+ ( filter . exclude . size && hasIntersection ( tagsOnNode , filter . exclude ) )
218
+ ) {
219
+ return {
220
+ ...node ,
221
+ directives : transformTagDirectives ( node , true ) ,
222
+ } ;
223
+ }
224
+
225
+ isSomeFieldsAccessible = true ;
176
226
177
- if (
178
- ( filter . include . size && ! hasIntersection ( tagsOnNode , filter . include ) ) ||
179
- ( filter . exclude . size && hasIntersection ( tagsOnNode , filter . exclude ) )
180
- ) {
181
227
return {
182
228
...node ,
183
- directives : transformTagDirectives ( node , true ) ,
229
+ directives : transformTagDirectives ( node ) ,
184
230
} ;
185
- }
186
-
187
- isAllFieldsInaccessible = false ;
188
-
189
- return {
190
- ...node ,
231
+ } ) ,
232
+ } as ObjectLikeNode ;
233
+
234
+ if ( filter . exclude . size && hasIntersection ( tagsOnNode , filter . exclude ) ) {
235
+ newNode = {
236
+ ...newNode ,
237
+ directives : transformTagDirectives (
238
+ node ,
239
+ typesWithInaccessibleApplied . has ( typeName ) ? false : true ,
240
+ ) ,
241
+ } as ObjectLikeNode ;
242
+ typesWithInaccessibleApplied . add ( typeName ) ;
243
+ } else {
244
+ newNode = {
245
+ ...newNode ,
191
246
directives : transformTagDirectives ( node ) ,
192
247
} ;
193
- } ) ,
194
- } ;
248
+ }
195
249
196
- if (
197
- ! rootTypeNames . has ( node . name . value ) &&
198
- filter . exclude . size &&
199
- hasIntersection ( tagsOnNode , filter . exclude )
200
- ) {
201
- return {
202
- ...newNode ,
203
- directives : transformTagDirectives ( node , true ) ,
204
- } ;
250
+ if ( ! firstReplacementTypeNodeRecord ) {
251
+ firstReplacementTypeNodeRecord = {
252
+ key : node ,
253
+ value : newNode ,
254
+ } ;
255
+ }
256
+ replacementTypeNodes . set ( node , newNode ) ;
205
257
}
206
258
207
- if ( isAllFieldsInaccessible ) {
208
- onAllFieldsInaccessible ( node . name . value ) ;
209
- } else {
210
- onSomeFieldsAccessible ( node . name . value ) ;
259
+ // If some fields are accessible, we continue with the next type
260
+ if ( isSomeFieldsAccessible ) {
261
+ onSomeFieldsAccessible ( typeName ) ;
262
+ continue ;
211
263
}
264
+ onAllFieldsInaccessible ( typeName ) ;
265
+ }
212
266
213
- return {
214
- ...newNode ,
215
- directives : transformTagDirectives ( node ) ,
216
- } ;
267
+ function fieldLikeObjectHandler ( node : ObjectLikeNode ) {
268
+ const newNode = replacementTypeNodes . get ( node ) ;
269
+ if ( ! newNode ) {
270
+ throw new Error (
271
+ `Found type without transformation mapping. ${ node . name . value } ${ node . name . kind } ` ,
272
+ ) ;
273
+ }
274
+
275
+ return newNode ;
217
276
}
218
277
219
278
function enumHandler ( node : EnumTypeDefinitionNode | EnumTypeExtensionNode ) {
@@ -304,6 +363,7 @@ export function applyTagFilterToInaccessibleTransformOnSubgraphSchema(
304
363
typeDefs,
305
364
typesWithAllFieldsInaccessible : typesWithAllFieldsInaccessibleTracker ,
306
365
transformTagDirectives,
366
+ typesWithInaccessibleApplied,
307
367
} ;
308
368
}
309
369
@@ -312,6 +372,8 @@ function makeTypesFromSetInaccessible(
312
372
types : Set < string > ,
313
373
transformTagDirectives : ReturnType < typeof createTransformTagDirectives > ,
314
374
) {
375
+ /** We can only apply @accessible once on each unique typename, otherwise we get a composition error */
376
+ const alreadyAppliedOnTypeNames = new Set < string > ( ) ;
315
377
function typeHandler (
316
378
node :
317
379
| ObjectTypeExtensionNode
@@ -323,9 +385,10 @@ function makeTypesFromSetInaccessible(
323
385
| EnumTypeDefinitionNode
324
386
| EnumTypeExtensionNode ,
325
387
) {
326
- if ( types . has ( node . name . value ) === false ) {
388
+ if ( types . has ( node . name . value ) === false || alreadyAppliedOnTypeNames . has ( node . name . value ) ) {
327
389
return ;
328
390
}
391
+ alreadyAppliedOnTypeNames . add ( node . name . value ) ;
329
392
return {
330
393
...node ,
331
394
directives : transformTagDirectives ( node , true ) ,
@@ -392,12 +455,26 @@ export function applyTagFilterOnSubgraphs<
392
455
...subgraph ,
393
456
typeDefs : makeTypesFromSetInaccessible (
394
457
subgraph . typeDefs ,
395
- intersectionOfTypesWhereAllFieldsAreInaccessible ,
458
+ /** We exclude the types that are already marked as inaccessible within the subgraph as we want to avoid `@inaccessible` applied more than once. */
459
+ difference (
460
+ intersectionOfTypesWhereAllFieldsAreInaccessible ,
461
+ subgraph . typesWithInaccessibleApplied ,
462
+ ) ,
396
463
subgraph . transformTagDirectives ,
397
464
) ,
398
465
} ) ) ;
399
466
}
400
467
468
+ function difference < $Type > ( set1 : Set < $Type > , set2 : Set < $Type > ) : Set < $Type > {
469
+ const result = new Set < $Type > ( ) ;
470
+ set1 . forEach ( item => {
471
+ if ( ! set2 . has ( item ) ) {
472
+ result . add ( item ) ;
473
+ }
474
+ } ) ;
475
+ return result ;
476
+ }
477
+
401
478
export const extractTagsFromDocument = (
402
479
documentNode : DocumentNode ,
403
480
tagStrategy : TagExtractionStrategy ,
0 commit comments