@@ -42,6 +42,7 @@ import {
42
42
} from './selection-set-processor/base' ;
43
43
import autoBind from 'auto-bind' ;
44
44
import { getRootTypes } from '@graphql-tools/utils' ;
45
+ import { createHash } from 'crypto' ;
45
46
46
47
type FragmentSpreadUsage = {
47
48
fragmentName : string ;
@@ -304,35 +305,120 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
304
305
// in case there is not a selection for each type, we need to add a empty type.
305
306
let mustAddEmptyObject = false ;
306
307
307
- const grouped = getPossibleTypes ( this . _schema , this . _parentSchemaType ) . reduce ( ( prev , type ) => {
308
- const typeName = type . name ;
309
- const schemaType = this . _schema . getType ( typeName ) ;
308
+ const possibleTypes = getPossibleTypes ( this . _schema , this . _parentSchemaType ) ;
310
309
311
- if ( ! isObjectType ( schemaType ) ) {
312
- throw new TypeError ( `Invalid state! Schema type ${ typeName } is not a valid GraphQL object!` ) ;
313
- }
310
+ if ( ! this . _config . mergeFragmentTypes || this . _config . inlineFragmentTypes === 'mask' ) {
311
+ const grouped = possibleTypes . reduce ( ( prev , type ) => {
312
+ const typeName = type . name ;
313
+ const schemaType = this . _schema . getType ( typeName ) ;
314
314
315
- const selectionNodes = selectionNodesByTypeName . get ( typeName ) || [ ] ;
315
+ if ( ! isObjectType ( schemaType ) ) {
316
+ throw new TypeError ( `Invalid state! Schema type ${ typeName } is not a valid GraphQL object!` ) ;
317
+ }
316
318
317
- if ( ! prev [ typeName ] ) {
318
- prev [ typeName ] = [ ] ;
319
- }
319
+ const selectionNodes = selectionNodesByTypeName . get ( typeName ) || [ ] ;
320
320
321
- const transformedSet = this . buildSelectionSetString ( schemaType , selectionNodes ) ;
321
+ if ( ! prev [ typeName ] ) {
322
+ prev [ typeName ] = [ ] ;
323
+ }
322
324
323
- if ( transformedSet ) {
324
- prev [ typeName ] . push ( transformedSet ) ;
325
- } else {
326
- mustAddEmptyObject = true ;
327
- }
325
+ const { fields } = this . buildSelectionSet ( schemaType , selectionNodes ) ;
326
+ const transformedSet = this . selectionSetStringFromFields ( fields ) ;
327
+
328
+ if ( transformedSet ) {
329
+ prev [ typeName ] . push ( transformedSet ) ;
330
+ } else {
331
+ mustAddEmptyObject = true ;
332
+ }
333
+
334
+ return prev ;
335
+ } , { } as Record < string , string [ ] > ) ;
336
+
337
+ return { grouped, mustAddEmptyObject } ;
338
+ } else {
339
+ // Accumulate a map of selected fields to the typenames that
340
+ // share the exact same selected fields. When we find multiple
341
+ // typenames with the same set of fields, we can collapse the
342
+ // generated type to the selected fields and a string literal
343
+ // union of the typenames.
344
+ //
345
+ // E.g. {
346
+ // __typename: "foo" | "bar";
347
+ // shared: string;
348
+ // }
349
+ const grouped = possibleTypes . reduce <
350
+ Record < string , { fields : ( string | NameAndType ) [ ] ; types : { name : string ; type : string } [ ] } >
351
+ > ( ( prev , type ) => {
352
+ const typeName = type . name ;
353
+ const schemaType = this . _schema . getType ( typeName ) ;
354
+
355
+ if ( ! isObjectType ( schemaType ) ) {
356
+ throw new TypeError ( `Invalid state! Schema type ${ typeName } is not a valid GraphQL object!` ) ;
357
+ }
328
358
329
- return prev ;
330
- } , { } as Record < string , string [ ] > ) ;
359
+ const selectionNodes = selectionNodesByTypeName . get ( typeName ) || [ ] ;
360
+
361
+ const { typeInfo, fields } = this . buildSelectionSet ( schemaType , selectionNodes ) ;
362
+
363
+ const key = this . selectionSetStringFromFields ( fields ) ;
364
+ prev [ key ] = {
365
+ fields,
366
+ types : [ ...( prev [ key ] ?. types ?? [ ] ) , typeInfo || { name : '' , type : type . name } ] . filter ( Boolean ) ,
367
+ } ;
368
+
369
+ return prev ;
370
+ } , { } ) ;
371
+
372
+ // For every distinct set of fields, create the corresponding
373
+ // string literal union of typenames.
374
+ const compacted = Object . keys ( grouped ) . reduce < Record < string , string [ ] > > ( ( acc , key ) => {
375
+ const typeNames = grouped [ key ] . types . map ( t => t . type ) ;
376
+ // Don't create very large string literal unions. TypeScript
377
+ // will stop comparing some nested union types types when
378
+ // they contain props with more than some number of string
379
+ // literal union members (testing with TS 4.5 stops working
380
+ // at 25 for a naive test case:
381
+ // https://www.typescriptlang.org/play?ts=4.5.4&ssl=29&ssc=10&pln=29&pc=1#code/C4TwDgpgBAKg9nAMgQwE4HNoF4BQV9QA+UA3ngRQJYB21EqAXDsQEQCMLzULATJ6wGZ+3ACzCWAVnEA2cQHZxADnEBOcWwAM6jl3Z9dbIQbEGpB2QYUHlBtbp5b7O1j30ujLky7Os4wABb0nAC+ODigkFAAQlBYUOT4xGQUVLT0TKzO3G7cHqLiPtwWrFasNqx2mY6ZWXrqeexe3GyF7MXNpc3lzZXZ1dm1ruI8DTxNvGahFEkJKTR0jLMpRNx+gaicy6E4APQ7AALAAM4AtJTo1HCoEDgANhDAUMgMsAgoGNikwQDcdw9QACMXjE4shfmEItAAGI0bCzGbLfDzdIGYbiBrjVrtFidFjdFi9dj9di1Ng5dgNNjjFrqbFsXFsfFsQkOYaDckjYbjNZBHDbPaHU7nS7XP6PZBsF4wuixL6-e6PAGS6KyiXfIA
382
+ const max_types = 20 ;
383
+ for ( let i = 0 ; i < typeNames . length ; i += max_types ) {
384
+ const selectedTypes = typeNames . slice ( i , i + max_types ) ;
385
+ const typenameUnion = grouped [ key ] . types [ 0 ] . name
386
+ ? this . _processor . transformTypenameField ( selectedTypes . join ( ' | ' ) , grouped [ key ] . types [ 0 ] . name )
387
+ : [ ] ;
388
+ const transformedSet = this . selectionSetStringFromFields ( [ ...typenameUnion , ...grouped [ key ] . fields ] ) ;
389
+
390
+ // The keys here will be used to generate intermediary
391
+ // fragment names. To avoid blowing up the type name on large
392
+ // unions, calculate a stable hash here instead.
393
+ //
394
+ // Also use fragment hashing if skipTypename is true, since we
395
+ // then don't have a typename for naming the fragment.
396
+ acc [
397
+ selectedTypes . length <= 3
398
+ ? selectedTypes . join ( '_' )
399
+ : createHash ( 'sha256' )
400
+ . update ( selectedTypes . join ( ) || transformedSet || '' )
401
+ . digest ( 'base64' )
402
+ ] = [ transformedSet ] ;
403
+ }
404
+ return acc ;
405
+ } , { } ) ;
406
+
407
+ return { grouped : compacted , mustAddEmptyObject } ;
408
+ }
409
+ }
331
410
332
- return { grouped, mustAddEmptyObject } ;
411
+ protected selectionSetStringFromFields ( fields : ( string | NameAndType ) [ ] ) : string | null {
412
+ const allStrings = fields . filter ( ( f : string | NameAndType ) : f is string => typeof f === 'string' ) ;
413
+ const allObjects = fields
414
+ . filter ( ( f : string | NameAndType ) : f is NameAndType => typeof f !== 'string' )
415
+ . map ( t => `${ t . name } : ${ t . type } ` ) ;
416
+ const mergedObjects = allObjects . length ? this . _processor . buildFieldsIntoObject ( allObjects ) : null ;
417
+ const transformedSet = this . _processor . buildSelectionSetFromStrings ( [ ...allStrings , mergedObjects ] . filter ( Boolean ) ) ;
418
+ return transformedSet ;
333
419
}
334
420
335
- protected buildSelectionSetString (
421
+ protected buildSelectionSet (
336
422
parentSchemaType : GraphQLObjectType ,
337
423
selectionNodes : Array < SelectionNode | FragmentSpreadUsage | DirectiveNode >
338
424
) {
@@ -461,7 +547,12 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
461
547
this . _config . skipTypeNameForRoot
462
548
) ;
463
549
const transformed : ProcessResult = [
464
- ...( typeInfoField ? this . _processor . transformTypenameField ( typeInfoField . type , typeInfoField . name ) : [ ] ) ,
550
+ // Only add the typename field if we're not merging fragment
551
+ // types. If we are merging, we need to wait until we know all
552
+ // the involved typenames.
553
+ ...( typeInfoField && ( ! this . _config . mergeFragmentTypes || this . _config . inlineFragmentTypes === 'mask' )
554
+ ? this . _processor . transformTypenameField ( typeInfoField . type , typeInfoField . name )
555
+ : [ ] ) ,
465
556
...this . _processor . transformPrimitiveFields (
466
557
parentSchemaType ,
467
558
Array . from ( primitiveFields . values ( ) ) . map ( field => ( {
@@ -499,7 +590,7 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
499
590
}
500
591
}
501
592
502
- return this . _processor . buildSelectionSetFromStrings ( fields ) ;
593
+ return { typeInfo : typeInfoField , fields } ;
503
594
}
504
595
505
596
protected buildTypeNameField (
0 commit comments