From 985d60524efb7cfebb681609ee3555fdb5762a1c Mon Sep 17 00:00:00 2001 From: haru0017 Date: Tue, 24 Feb 2026 10:16:58 +0900 Subject: [PATCH] support custom path per error --- executor_test.go | 95 ++++++++++++++++++++++++++++++++++++++++++ gqlerrors/formatted.go | 12 +++++- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/executor_test.go b/executor_test.go index 38106b0..f5a86fb 100644 --- a/executor_test.go +++ b/executor_test.go @@ -2470,3 +2470,98 @@ func TestQuery_NestedJoinedErrors(t *testing.T) { } } } + +type pathError struct { + msg string + path []interface{} +} + +func (e *pathError) Error() string { return e.msg } +func (e *pathError) Path() []interface{} { return e.path } + +func TestQuery_CustomPathError(t *testing.T) { + customPath := []interface{}{"custom", "path", "to", "field"} + result := testErrors(t, graphql.String, nil, func(err error) error { + return &pathError{msg: err.Error(), path: customPath} + }) + + if len(result.Errors) != 1 { + t.Fatalf("expected 1 error, got %d: %+v", len(result.Errors), result.Errors) + } + + if !reflect.DeepEqual(result.Errors[0].Path, customPath) { + t.Errorf("expected path %v, got %v", customPath, result.Errors[0].Path) + } +} + +type pathAndExtendedError struct { + msg string + path []interface{} + extensions map[string]interface{} +} + +func (e *pathAndExtendedError) Error() string { return e.msg } +func (e *pathAndExtendedError) Path() []interface{} { return e.path } +func (e *pathAndExtendedError) Extensions() map[string]interface{} { return e.extensions } + +func TestQuery_PathErrorWithExtensions(t *testing.T) { + customPath := []interface{}{"input", "name"} + customExtensions := map[string]interface{}{"code": "VALIDATION_ERROR"} + + result := testErrors(t, graphql.String, nil, func(err error) error { + return &pathAndExtendedError{ + msg: err.Error(), + path: customPath, + extensions: customExtensions, + } + }) + + if len(result.Errors) != 1 { + t.Fatalf("expected 1 error, got %d: %+v", len(result.Errors), result.Errors) + } + + if !reflect.DeepEqual(result.Errors[0].Path, customPath) { + t.Errorf("expected path %v, got %v", customPath, result.Errors[0].Path) + } + + if !reflect.DeepEqual(result.Errors[0].Extensions, customExtensions) { + t.Errorf("expected extensions %v, got %v", customExtensions, result.Errors[0].Extensions) + } +} + +func TestQuery_MultipleErrorsWithCustomPaths(t *testing.T) { + path1 := []interface{}{"input", "name"} + path2 := []interface{}{"input", "age"} + + result := testErrors(t, graphql.String, nil, func(err error) error { + err1 := &pathError{msg: "name is required", path: path1} + err2 := &pathError{msg: "age must be positive", path: path2} + return errors.Join(err1, err2) + }) + + if len(result.Errors) != 2 { + t.Fatalf("expected 2 errors, got %d: %+v", len(result.Errors), result.Errors) + } + + if !reflect.DeepEqual(result.Errors[0].Path, path1) { + t.Errorf("error[0]: expected path %v, got %v", path1, result.Errors[0].Path) + } + + if !reflect.DeepEqual(result.Errors[1].Path, path2) { + t.Errorf("error[1]: expected path %v, got %v", path2, result.Errors[1].Path) + } +} + +func TestQuery_PathErrorReturnsNil(t *testing.T) { + result := testErrors(t, graphql.String, nil, func(err error) error { + return &pathError{msg: err.Error(), path: nil} + }) + + if len(result.Errors) != 1 { + t.Fatalf("expected 1 error, got %d: %+v", len(result.Errors), result.Errors) + } + + if result.Errors[0].Path != nil { + t.Errorf("expected path to be nil, got %v", result.Errors[0].Path) + } +} diff --git a/gqlerrors/formatted.go b/gqlerrors/formatted.go index 156e0fe..43b29e4 100644 --- a/gqlerrors/formatted.go +++ b/gqlerrors/formatted.go @@ -11,6 +11,11 @@ type ExtendedError interface { Extensions() map[string]interface{} } +type PathError interface { + error + Path() []interface{} +} + type FormattedError struct { Message string `json:"message"` Locations []location.SourceLocation `json:"locations"` @@ -43,10 +48,13 @@ func FormatError(err error) FormattedError { Path: err.Path, originalError: err, } - if err := err.OriginalError; err != nil { - if extended, ok := err.(ExtendedError); ok { + if origErr := err.OriginalError; origErr != nil { + if extended, ok := origErr.(ExtendedError); ok { ret.Extensions = extended.Extensions() } + if pathErr, ok := origErr.(PathError); ok { + ret.Path = pathErr.Path() + } } return ret case Error: