@@ -19,12 +19,23 @@ import {
1919 visit ,
2020 visitWithTypeInfo ,
2121 BREAK ,
22+ ASTNode ,
2223} from "graphql" ;
2324
2425import { inlineNamedFragmentSpreads } from "../utils/inline-named-fragment-spreads.js" ;
2526
27+ export interface FixtureInputValidationError {
28+ message : string ;
29+ path : ( string | number ) [ ] ;
30+ }
31+
2632export interface ValidateFixtureInputResult {
27- errors : string [ ] ;
33+ errors : FixtureInputValidationError [ ] ;
34+ }
35+
36+ interface NestedValue {
37+ value : any ;
38+ path : ( string | number ) [ ] ;
2839}
2940
3041/**
@@ -46,15 +57,15 @@ export function validateFixtureInput(
4657) : ValidateFixtureInputResult {
4758 const inlineFragmentSpreadsAst = inlineNamedFragmentSpreads ( queryAST ) ;
4859 const typeInfo = new TypeInfo ( schema ) ;
49- const valueStack : any [ ] [ ] = [ [ value ] ] ;
60+ const valueStack : NestedValue [ ] [ ] = [ [ { value, path : [ ] } ] ] ;
5061 const typeStack : ( GraphQLNamedType | undefined ) [ ] = [ ] ;
5162 const possibleTypesStack : Set < string > [ ] = [
5263 new Set ( [ schema . getQueryType ( ) ! . name ] ) ,
5364 ] ;
5465 const typenameResponseKeyStack : ( string | undefined ) [ ] = [ ] ;
5566 const expectedFieldsStack : Map < string , Set < string > > [ ] = [ new Map ( ) ] ;
5667
57- const errors : string [ ] = [ ] ;
68+ const errors : FixtureInputValidationError [ ] = [ ] ;
5869
5970 visit (
6071 inlineFragmentSpreadsAst ,
@@ -87,7 +98,7 @@ export function validateFixtureInput(
8798 } ,
8899 } ,
89100 Field : {
90- enter ( node ) {
101+ enter ( node , _key , _parent , _path , ancestors ) {
91102 const currentValues = valueStack [ valueStack . length - 1 ] ;
92103 const nestedValues = [ ] ;
93104
@@ -108,14 +119,19 @@ export function validateFixtureInput(
108119
109120 const fieldDefinition = typeInfo . getFieldDef ( ) ;
110121 if ( fieldDefinition === undefined || fieldDefinition === null ) {
111- errors . push (
112- `Cannot validate ${ responseKey } : missing field definition` ,
113- ) ;
122+ const currentPath = pathFromAncestors ( ancestors ) ;
123+ errors . push ( {
124+ message : `Cannot validate ${ responseKey } : missing field definition` ,
125+ path : [ ...currentPath , responseKey ] ,
126+ } ) ;
114127 return BREAK ;
115128 }
116129 const fieldType = fieldDefinition . type ;
117130
118- for ( const currentValue of currentValues ) {
131+ for ( const {
132+ value : currentValue ,
133+ path : currentPath ,
134+ } of currentValues ) {
119135 const valueForResponseKey = currentValue [ responseKey ] ;
120136
121137 // Field is missing from fixture
@@ -134,7 +150,10 @@ export function validateFixtureInput(
134150 typenameResponseKey ,
135151 )
136152 ) {
137- errors . push ( `Missing expected fixture data for ${ responseKey } ` ) ;
153+ errors . push ( {
154+ message : `Missing expected fixture data for ${ responseKey } ` ,
155+ path : [ ...currentPath , responseKey ] ,
156+ } ) ;
138157 }
139158 }
140159 // Scalars and Enums (including wrapped types)
@@ -146,8 +165,11 @@ export function validateFixtureInput(
146165 coerceInputValue (
147166 valueForResponseKey ,
148167 fieldType ,
149- ( path , _invalidValue , error ) => {
150- errors . push ( `${ error . message } At "${ path . join ( "." ) } "` ) ;
168+ ( _path , _invalidValue , error ) => {
169+ errors . push ( {
170+ message : error . message ,
171+ path : [ ...currentPath , responseKey ] ,
172+ } ) ;
151173 } ,
152174 ) ;
153175 }
@@ -169,14 +191,15 @@ export function validateFixtureInput(
169191 processNestedArrays (
170192 valueForResponseKey ,
171193 unwrappedFieldType ,
172- responseKey ,
194+ [ ... currentPath , responseKey ] ,
173195 ) ;
174196 nestedValues . push ( ...flattened ) ;
175197 errors . push ( ...flattenErrors ) ;
176198 } else {
177- errors . push (
178- `Expected array for ${ responseKey } , but got ${ typeof valueForResponseKey } ` ,
179- ) ;
199+ errors . push ( {
200+ message : `Expected array, but got ${ typeof valueForResponseKey } ` ,
201+ path : [ ...currentPath , responseKey ] ,
202+ } ) ;
180203 }
181204 }
182205 // Objects - validate and add to traversal stack
@@ -185,29 +208,36 @@ export function validateFixtureInput(
185208 isAbstractType ( unwrappedFieldType )
186209 ) {
187210 if ( valueForResponseKey === null ) {
188- errors . push (
189- `Expected object for ${ responseKey } , but got null` ,
190- ) ;
211+ errors . push ( {
212+ message : `Expected object, but got null` ,
213+ path : [ ...currentPath , responseKey ] ,
214+ } ) ;
191215 } else if ( typeof valueForResponseKey === "object" ) {
192- nestedValues . push ( valueForResponseKey ) ;
216+ nestedValues . push ( {
217+ value : valueForResponseKey ,
218+ path : [ ...currentPath , responseKey ] ,
219+ } ) ;
193220 } else {
194- errors . push (
195- `Expected object for ${ responseKey } , but got ${ typeof valueForResponseKey } ` ,
196- ) ;
221+ errors . push ( {
222+ message : `Expected object, but got ${ typeof valueForResponseKey } ` ,
223+ path : [ ...currentPath , responseKey ] ,
224+ } ) ;
197225 }
198226 }
199227 // Unexpected type - defensive check that should never be reached
200228 else {
201- errors . push (
202- `Unexpected type for ${ responseKey } : ${ unwrappedFieldType } ` ,
203- ) ;
229+ errors . push ( {
230+ message : `Unexpected type, expected ${ unwrappedFieldType } ` ,
231+ path : [ ...currentPath , responseKey ] ,
232+ } ) ;
204233 }
205234 }
206235 // No type information - should not happen with valid query
207236 else {
208- errors . push (
209- `Cannot validate ${ responseKey } : missing type information` ,
210- ) ;
237+ errors . push ( {
238+ message : `Cannot validate ${ responseKey } : missing type information` ,
239+ path : [ ...currentPath , responseKey ] ,
240+ } ) ;
211241 }
212242 }
213243
@@ -236,7 +266,7 @@ export function validateFixtureInput(
236266 } ,
237267 } ,
238268 SelectionSet : {
239- enter ( node , _key , parent ) {
269+ enter ( node , _key , parent , _path , ancestors ) {
240270 // If this SelectionSet belongs to a Field, prepare to track expected fields
241271 if ( parent && "kind" in parent && parent . kind === Kind . FIELD ) {
242272 expectedFieldsStack . push ( new Map ( ) ) ;
@@ -281,9 +311,11 @@ export function validateFixtureInput(
281311 ) . length ;
282312
283313 if ( ! hasTypename && fragmentSpreadCount > 1 ) {
284- errors . push (
285- `Missing __typename field for abstract type ${ getNamedType ( typeInfo . getType ( ) ) ?. name } ` ,
286- ) ;
314+ const currentPath = pathFromAncestors ( [ ...ancestors , parent ! ] ) ;
315+ errors . push ( {
316+ message : `Missing __typename field for abstract type ${ getNamedType ( typeInfo . getType ( ) ) ?. name } ` ,
317+ path : currentPath ,
318+ } ) ;
287319 return BREAK ;
288320 }
289321 }
@@ -348,42 +380,43 @@ export function validateFixtureInput(
348380function processNestedArrays (
349381 value : any [ ] ,
350382 listType : GraphQLList < any > ,
351- fieldName : string ,
352- ) : { values : any [ ] ; errors : string [ ] } {
353- const result : any [ ] = [ ] ;
354- const errors : string [ ] = [ ] ;
383+ path : ( string | number ) [ ] ,
384+ ) : { values : NestedValue [ ] ; errors : FixtureInputValidationError [ ] } {
385+ const nestedValues : NestedValue [ ] = [ ] ;
386+ const errors : FixtureInputValidationError [ ] = [ ] ;
355387 const elementType = listType . ofType ;
356388
357389 for ( const [ index , element ] of value . entries ( ) ) {
358390 if ( element === null ) {
359391 if ( ! isNullableType ( elementType ) ) {
360- errors . push (
361- `Null value found in non-nullable array at ${ fieldName } [${ index } ]` ,
362- ) ;
392+ errors . push ( {
393+ message : "Null value found in non-nullable array" ,
394+ path : [ ...path , index ] ,
395+ } ) ;
363396 }
364397 } else if ( isListType ( elementType ) ) {
365398 // Element type is a list - expect nested array and recurse
366399 if ( Array . isArray ( element ) ) {
367- const nested = processNestedArrays (
368- element ,
369- elementType ,
370- `${ fieldName } [${ index } ]` ,
371- ) ;
372- result . push ( ...nested . values ) ;
400+ const nested = processNestedArrays ( element , elementType , [
401+ ...path ,
402+ index ,
403+ ] ) ;
404+ nestedValues . push ( ...nested . values ) ;
373405 errors . push ( ...nested . errors ) ;
374406 } else {
375407 // Error: fixture structure doesn't match schema nesting
376- errors . push (
377- `Expected array at ${ fieldName } [${ index } ], but got ${ typeof element } ` ,
378- ) ;
408+ errors . push ( {
409+ message : `Expected array, but got ${ typeof element } ` ,
410+ path : [ ...path , index ] ,
411+ } ) ;
379412 }
380413 } else {
381414 // Non-list type - add directly
382- result . push ( element ) ;
415+ nestedValues . push ( { value : element , path : [ ... path , index ] } ) ;
383416 }
384417 }
385418
386- return { values : result , errors } ;
419+ return { values : nestedValues , errors } ;
387420}
388421
389422/**
@@ -461,13 +494,13 @@ function isValueExpectedForType(
461494 * - No schema lookups needed - possible types were pre-computed during traversal
462495 */
463496function checkForExtraFields (
464- fixtureObjects : any [ ] ,
497+ fixtureObjects : NestedValue [ ] ,
465498 expectedFields : Map < string , Set < string > > ,
466499 typenameResponseKey : string | undefined ,
467- ) : string [ ] {
468- const errors : string [ ] = [ ] ;
500+ ) : FixtureInputValidationError [ ] {
501+ const errors : FixtureInputValidationError [ ] = [ ] ;
469502
470- for ( const fixtureObject of fixtureObjects ) {
503+ for ( const { value : fixtureObject , path } of fixtureObjects ) {
471504 if (
472505 typeof fixtureObject === "object" &&
473506 fixtureObject !== null &&
@@ -502,13 +535,22 @@ function checkForExtraFields(
502535 // Check each field in the fixture object
503536 for ( const fixtureField of fixtureFields ) {
504537 if ( ! expectedForThisObject . has ( fixtureField ) ) {
505- errors . push (
506- `Extra field "${ fixtureField } " found in fixture data not in query` ,
507- ) ;
538+ errors . push ( {
539+ message : `Extra field "${ fixtureField } " found in fixture data not in query` ,
540+ path : [ ...path , fixtureField ] ,
541+ } ) ;
508542 }
509543 }
510544 }
511545 }
512546
513547 return errors ;
514548}
549+
550+ function pathFromAncestors (
551+ ancestors : ReadonlyArray < ASTNode | ReadonlyArray < ASTNode > > ,
552+ ) : ( string | number ) [ ] {
553+ return ancestors
554+ . filter ( ( ancestor ) => "kind" in ancestor && ancestor . kind === Kind . FIELD )
555+ . map ( ( ancestor ) => ancestor . alias ?. value || ancestor . name . value ) ;
556+ }
0 commit comments