@@ -217,8 +217,8 @@ import {
217217 type CachePopulateConfig ,
218218 type EntityKeyMappingConfig ,
219219 type FieldMappingConfig ,
220- type RequestScopedFieldConfig ,
221220 type QueryCacheConfig ,
221+ type RequestScopedConfiguration ,
222222} from '../../router-configuration/types' ;
223223import { printTypeNode } from '@graphql-tools/merge' ;
224224import {
@@ -229,10 +229,10 @@ import {
229229 fieldAlreadyProvidedWarning ,
230230 invalidExternalFieldWarning ,
231231 nonExternalConditionalFieldWarning ,
232+ requestScopedSingleFieldWarning ,
232233 singleSubgraphInputFieldOneOfWarning ,
233234 unimplementedInterfaceOutputTypeWarning ,
234235 queryCacheReturnEntityMissingEntityCacheWarning ,
235- requestScopedSingleFieldWarning ,
236236} from '../warnings/warnings' ;
237237import { upsertDirectiveSchemaAndEntityDefinitions , upsertParentsAndChildren } from './walkers' ;
238238import {
@@ -542,6 +542,7 @@ export class NormalizationFactory {
542542 referencedDirectiveNames = new Set < DirectiveName > ( ) ;
543543 referencedTypeNames = new Set < TypeName > ( ) ;
544544 renamedParentTypeName = '' ;
545+ requestScopedFieldCoordsByL1Key = new Map < string , Array < string > > ( ) ;
545546 schemaData : SchemaData ;
546547 subgraphName : SubgraphName ;
547548 unvalidatedExternalFieldCoords = new Set < string > ( ) ;
@@ -5214,81 +5215,36 @@ export class NormalizationFactory {
52145215 return [ ] ;
52155216 }
52165217
5217- extractRequestScopedFields ( ) {
5218- // Gather fields annotated with @openfed__requestScoped across all types in this subgraph.
5219- // A field is both a reader and writer of the coordinate L1 — no receiver/provider.
5220- // Fields with the same `key` share the same L1 entry: whichever is resolved first
5221- // populates it, subsequent ones inject from it.
5222- type Collected = {
5223- typeName : string ;
5224- fieldName : string ;
5225- fieldCoords : string ;
5226- key : string ;
5227- l1Key : string ;
5228- } ;
5229- const collected : Array < Collected > = [ ] ;
5230-
5231- for ( const [ , parentData ] of this . parentDefinitionDataByTypeName ) {
5232- if ( parentData . kind !== Kind . OBJECT_TYPE_DEFINITION && parentData . kind !== Kind . INTERFACE_TYPE_DEFINITION ) {
5233- continue ;
5234- }
5235- const typeName = getParentTypeName ( parentData ) ;
5236- for ( const [ fieldName , fieldData ] of parentData . fieldDataByName ) {
5237- const directives = fieldData . directivesByName . get ( OPENFED_REQUEST_SCOPED ) ;
5238- if ( ! directives ) {
5239- continue ;
5240- }
5241- // validateDirectives() (run earlier in normalize()) has already guaranteed a single, non-repeated
5242- // @openfed__requestScoped with a required String `key`, so the generic ConstDirectiveNode can be
5243- // narrowed to the precise typed node — mirroring handleComposeDirective()/ComposeDirectiveNode.
5244- const directive = directives [ 0 ] as RequestScopedDirectiveNode ;
5245- const keyArg = directive . arguments . find ( ( arg ) => arg . name . value === KEY ) ;
5246- if ( ! keyArg ) {
5247- continue ;
5248- }
5249-
5250- const fieldCoords = `${ typeName } .${ fieldName } ` ;
5251- const key = keyArg . value . value ;
5252- const l1Key = `${ this . subgraphName } .${ key } ` ;
5253- collected . push ( { typeName, fieldName, fieldCoords, key, l1Key } ) ;
5254- }
5255- }
5256-
5257- if ( collected . length === 0 ) {
5218+ // Attaches a single field annotated with @openfed__requestScoped to its type's configurationData. A field
5219+ // is both a reader and writer of the coordinate L1 — no receiver/provider. Fields with the same `key` share
5220+ // the same L1 entry: whichever is resolved first populates it, subsequent ones inject from it.
5221+ extractRequestScopedConfig ( fieldData : FieldData ) {
5222+ const directives = fieldData . directivesByName . get ( OPENFED_REQUEST_SCOPED ) ;
5223+ if ( ! directives ) {
52585224 return ;
52595225 }
5260-
5261- // Warn when a key is used on only one field — @openfed__requestScoped is meaningless
5262- // unless >= 2 fields share the key (there'd be no second reader to benefit).
5263- const coordsByKey = new Map < string , Array < string > > ( ) ;
5264- for ( const c of collected ) {
5265- getValueOrDefault ( coordsByKey , c . key , ( ) => [ ] ) . push ( c . fieldCoords ) ;
5266- }
5267- for ( const [ key , coordsList ] of coordsByKey ) {
5268- if ( coordsList . length === 1 ) {
5269- this . warnings . push (
5270- requestScopedSingleFieldWarning ( {
5271- subgraphName : this . subgraphName ,
5272- key,
5273- fieldCoords : coordsList [ 0 ] ,
5274- } ) ,
5275- ) ;
5276- }
5226+ // validateDirectives() (run earlier in normalize()) has already guaranteed a single, non-repeated
5227+ // @openfed__requestScoped with a required String `key`, so the generic ConstDirectiveNode can be
5228+ // narrowed to the precise typed node — mirroring handleComposeDirective()/ComposeDirectiveNode.
5229+ const directive = directives [ 0 ] as RequestScopedDirectiveNode ;
5230+ const keyArg = directive . arguments . find ( ( arg ) => arg . name . value === KEY ) ;
5231+ if ( ! keyArg ) {
5232+ return ;
52775233 }
52785234
5279- // Group by type and attach to configurationData.
5280- const byType = new Map < string , Array < RequestScopedFieldConfig > > ( ) ;
5281- for ( const c of collected ) {
5282- const list = byType . get ( c . typeName ) ?? [ ] ;
5283- list . push ( { fieldName : c . fieldName , typeName : c . typeName , l1Key : c . l1Key } ) ;
5284- byType . set ( c . typeName , list ) ;
5285- }
5286- for ( const [ typeName , fields ] of byType ) {
5287- const configurationData = getValueOrDefault ( this . configurationDataByTypeName , typeName , ( ) =>
5288- newConfigurationData ( false , typeName ) ,
5289- ) ;
5290- getOrInitializeEntityCaching ( configurationData ) . requestScopedFields = fields ;
5291- }
5235+ const config : RequestScopedConfiguration = {
5236+ fieldName : fieldData . name ,
5237+ typeName : fieldData . renamedParentTypeName ,
5238+ l1Key : ` ${ this . subgraphName } . ${ keyArg . value . value } ` ,
5239+ } ;
5240+ const configurationData = getValueOrDefault ( this . configurationDataByTypeName , fieldData . renamedParentTypeName , ( ) =>
5241+ newConfigurationData ( false , fieldData . renamedParentTypeName ) ,
5242+ ) ;
5243+ getOrInitializeEntityCaching ( configurationData ) . requestScopedConfigurations . push ( config ) ;
5244+ // Track field coords per L1 key so the single-field warning can be emitted after the walk completes.
5245+ getValueOrDefault ( this . requestScopedFieldCoordsByL1Key , config . l1Key , ( ) => [ ] ) . push (
5246+ ` ${ config . typeName } . ${ config . fieldName } ` ,
5247+ ) ;
52925248 }
52935249
52945250 addFieldNamesToConfigurationData ( fieldDataByFieldName : Map < string , FieldData > , configurationData : ConfigurationData ) {
@@ -5483,6 +5439,8 @@ export class NormalizationFactory {
54835439 this . extractCacheInvalidateConfig ( fieldData ) ;
54845440 this . extractCachePopulateConfig ( fieldData ) ;
54855441 }
5442+
5443+ this . extractRequestScopedConfig ( fieldData ) ;
54865444 } else if ( fieldData . externalFieldDataBySubgraphName . get ( this . subgraphName ) ?. isDefinedExternal ) {
54875445 externalInterfaceFieldNames . push ( fieldName ) ;
54885446 }
@@ -5573,6 +5531,21 @@ export class NormalizationFactory {
55735531 }
55745532 }
55755533 }
5534+ // @openfed__requestScoped is meaningless unless >= 2 fields share a key (there'd be no second reader to
5535+ // benefit), so warn for any key used on only one field across the subgraph. extractRequestScopedConfig()
5536+ // populated requestScopedFieldCoordsByL1Key during the type walk above.
5537+ for ( const [ l1Key , fieldCoordsList ] of this . requestScopedFieldCoordsByL1Key ) {
5538+ if ( fieldCoordsList . length === 1 ) {
5539+ this . warnings . push (
5540+ requestScopedSingleFieldWarning ( {
5541+ subgraphName : this . subgraphName ,
5542+ // l1Key is `${this.subgraphName}.${key}`, so strip the prefix to recover the original key.
5543+ key : l1Key . slice ( this . subgraphName . length + 1 ) ,
5544+ fieldCoords : fieldCoordsList [ 0 ] ,
5545+ } ) ,
5546+ ) ;
5547+ }
5548+ }
55765549 this . isSubgraphEventDrivenGraph = this . edfsDirectiveReferences . size > 0 ;
55775550 // this is where @provides and @requires configurations are added to the ConfigurationData
55785551 this . addValidConditionalFieldSetConfigurations ( ) ;
@@ -5581,8 +5554,6 @@ export class NormalizationFactory {
55815554 // @openfed__queryCache extraction and @openfed__is placement validation — must run after key field sets
55825555 // are available (queryCache reads keyFieldSetDatasByTypeName to build argument→key mappings)
55835556 this . processRootFieldCacheDirectives ( ) ;
5584- // this is where @openfed__requestScoped configurations are added to the ConfigurationData
5585- this . extractRequestScopedFields ( ) ;
55865557 // Check that explicitly defined operations types are valid objects and that their fields are also valid
55875558 for ( const operationType of Object . values ( OperationTypeNode ) ) {
55885559 const operationTypeNode = this . schemaData . operationTypes . get ( operationType ) ;
0 commit comments