From 820d2a4bb69189940b0be4bc0d72180e1ae75c1e Mon Sep 17 00:00:00 2001 From: steebchen Date: Sat, 28 Oct 2023 05:23:04 +0400 Subject: [PATCH] use user facing errors, refactor --- engine/mock/do.go | 5 ++- engine/{ => protocol}/protocol.go | 29 ++++++++++++++--- engine/proxy.go | 3 +- engine/request.go | 16 ++++++--- runtime/builder/builder.go | 3 +- runtime/transaction/transaction.go | 11 ++++--- runtime/types/errors.go | 52 +++++++++++++++++------------- test/errors/unique/unique_test.go | 2 +- test/test.go | 5 +-- 9 files changed, 80 insertions(+), 46 deletions(-) rename engine/{ => protocol}/protocol.go (60%) diff --git a/engine/mock/do.go b/engine/mock/do.go index 744c30f17..6b8322cc1 100644 --- a/engine/mock/do.go +++ b/engine/mock/do.go @@ -4,8 +4,7 @@ import ( "context" "encoding/json" "fmt" - - "github.com/steebchen/prisma-client-go/engine" + "github.com/steebchen/prisma-client-go/engine/protocol" ) func (e *Engine) Do(_ context.Context, payload interface{}, v interface{}) error { @@ -16,7 +15,7 @@ func (e *Engine) Do(_ context.Context, payload interface{}, v interface{}) error n := -1 for i, e := range expectations { - req := payload.(engine.GQLRequest) + req := payload.(protocol.GQLRequest) if e.Query.Build() == req.Query { n = i break diff --git a/engine/protocol.go b/engine/protocol/protocol.go similarity index 60% rename from engine/protocol.go rename to engine/protocol/protocol.go index b20faf783..7bc6727ed 100644 --- a/engine/protocol.go +++ b/engine/protocol/protocol.go @@ -1,4 +1,4 @@ -package engine +package protocol import ( "encoding/json" @@ -33,11 +33,30 @@ type GQLBatchRequest struct { Transaction bool `json:"transaction"` } -// GQLError is a GraphQL Error +type UserFacingError struct { + IsPanic bool `json:"is_panic"` + Message string `json:"message"` + Meta Meta `json:"meta"` + ErrorCode string `json:"error_code"` +} + +func (e *UserFacingError) Error() string { + return e.Message +} + +type Meta struct { + Target interface{} `json:"target"` // can be of type []string or string +} + +// GQLError is a GraphQL Message type GQLError struct { - Message string `json:"error"` // note: the query-engine uses 'error' instead of 'message' - Path []string `json:"path"` - Extensions map[string]interface{} `json:"query"` + Message string `json:"error"` + UserFacingError *UserFacingError `json:"user_facing_error"` + Path []string `json:"path"` +} + +func (e *GQLError) Error() string { + return e.Message } func (e *GQLError) RawMessage() string { diff --git a/engine/proxy.go b/engine/proxy.go index b3d55234c..b27c63fdb 100644 --- a/engine/proxy.go +++ b/engine/proxy.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/steebchen/prisma-client-go/engine/protocol" "net/http" "net/url" "path" @@ -108,7 +109,7 @@ func (e *DataProxyEngine) Do(ctx context.Context, payload interface{}, into inte startParse := time.Now() - var response GQLResponse + var response protocol.GQLResponse if err := json.Unmarshal(body, &response); err != nil { return fmt.Errorf("json gql resopnse unmarshal: %w", err) } diff --git a/engine/request.go b/engine/request.go index 8c0464d8e..ad1fd18a7 100644 --- a/engine/request.go +++ b/engine/request.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/steebchen/prisma-client-go/engine/protocol" "net/http" "time" @@ -28,18 +29,23 @@ func (e *QueryEngine) Do(ctx context.Context, payload interface{}, v interface{} startParse := time.Now() - var response GQLResponse + var response protocol.GQLResponse if err := json.Unmarshal(body, &response); err != nil { return fmt.Errorf("json gql response unmarshal: %w", err) } if len(response.Errors) > 0 { - first := response.Errors[0] - if first.RawMessage() == internalUpdateNotFoundMessage || - first.RawMessage() == internalDeleteNotFoundMessage { + e := response.Errors[0] + if e.RawMessage() == internalUpdateNotFoundMessage || + e.RawMessage() == internalDeleteNotFoundMessage { return types.ErrNotFound } - return fmt.Errorf("pql error: %s", first.RawMessage()) + + if e.UserFacingError != nil { + return fmt.Errorf("user facing error: %w", e.UserFacingError) + } + + return fmt.Errorf("internal error: %s", e.RawMessage()) } response.Data.Result, err = transformResponse(response.Data.Result) diff --git a/runtime/builder/builder.go b/runtime/builder/builder.go index 6a7f3ad2e..f192a677a 100644 --- a/runtime/builder/builder.go +++ b/runtime/builder/builder.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/steebchen/prisma-client-go/engine/protocol" "strings" "time" @@ -243,7 +244,7 @@ func (q Query) buildFields(list bool, wrapList bool, fields []Field) string { } func (q Query) Exec(ctx context.Context, into interface{}) error { - payload := engine.GQLRequest{ + payload := protocol.GQLRequest{ Query: q.Build(), Variables: map[string]interface{}{}, } diff --git a/runtime/transaction/transaction.go b/runtime/transaction/transaction.go index e783c990e..3686231ca 100644 --- a/runtime/transaction/transaction.go +++ b/runtime/transaction/transaction.go @@ -3,6 +3,7 @@ package transaction import ( "context" "fmt" + "github.com/steebchen/prisma-client-go/engine/protocol" "github.com/steebchen/prisma-client-go/engine" "github.com/steebchen/prisma-client-go/runtime/builder" @@ -18,9 +19,9 @@ type Param interface { } func (r TX) Transaction(queries ...Param) Exec { - requests := make([]engine.GQLRequest, len(queries)) + requests := make([]protocol.GQLRequest, len(queries)) for i, query := range queries { - requests[i] = engine.GQLRequest{ + requests[i] = protocol.GQLRequest{ Query: query.ExtractQuery().Build(), Variables: map[string]interface{}{}, } @@ -35,7 +36,7 @@ func (r TX) Transaction(queries ...Param) Exec { type Exec struct { queries []Param engine engine.Engine - requests []engine.GQLRequest + requests []protocol.GQLRequest } func (r Exec) Exec(ctx context.Context) error { @@ -44,8 +45,8 @@ func (r Exec) Exec(ctx context.Context) error { defer close(q.ExtractQuery().TxResult) } - var result engine.GQLBatchResponse - payload := engine.GQLBatchRequest{ + var result protocol.GQLBatchResponse + payload := protocol.GQLBatchRequest{ Batch: r.requests, Transaction: true, } diff --git a/runtime/types/errors.go b/runtime/types/errors.go index 97ffe482d..7c4144df0 100644 --- a/runtime/types/errors.go +++ b/runtime/types/errors.go @@ -2,7 +2,7 @@ package types import ( "errors" - "regexp" + "github.com/steebchen/prisma-client-go/engine/protocol" ) // ErrNotFound gets returned when a database record does not exist @@ -13,17 +13,14 @@ type F interface { } type ErrUniqueConstraint[T F] struct { - // Field only shows on Postgres - Field T + // Message is the error message + Message string + // Fields only shows on Postgres + Fields []T // Key only shows on MySQL Key string } -const fieldKey = "field" - -var prismaMySQLUniqueConstraint = regexp.MustCompile("Unique constraint failed on the constraint: `(?P<" + fieldKey + ">.+)`") -var prismaPostgresUniqueConstraint = regexp.MustCompile("Unique constraint failed on the fields: \\(`(?P<" + fieldKey + ">.+)`\\)") - // CheckUniqueConstraint returns on a unique constraint error or violation with error info // Use as follows: // @@ -36,26 +33,35 @@ var prismaPostgresUniqueConstraint = regexp.MustCompile("Unique constraint faile // // Ideally this will be replaced with Prisma-generated errors in the future func CheckUniqueConstraint[T F](err error) (*ErrUniqueConstraint[T], bool) { - if match, ok := findMatch(err, prismaMySQLUniqueConstraint); ok { - return &ErrUniqueConstraint[T]{ - Key: match, - }, true + var ufr *protocol.UserFacingError + if ok := errors.As(err, &ufr); !ok { + return nil, false + } + + if ufr.ErrorCode != "P2002" { + return nil, false } - if match, ok := findMatch(err, prismaPostgresUniqueConstraint); ok { + + // postgres + if items, ok := ufr.Meta.Target.([]interface{}); ok { + var fields []T + for _, f := range items { + field, ok := f.(string) + if ok { + fields = append(fields, T(field)) + } + } return &ErrUniqueConstraint[T]{ - Field: T(match), + Fields: fields, }, true } - return nil, false -} -func findMatch(err error, regex *regexp.Regexp) (string, bool) { - result := regex.FindStringSubmatch(err.Error()) - if result == nil { - return "", false + // mysql + if item, ok := ufr.Meta.Target.(string); ok { + return &ErrUniqueConstraint[T]{ + Key: item, + }, true } - index := regex.SubexpIndex(fieldKey) - field := result[index] - return field, true + return nil, false } diff --git a/test/errors/unique/unique_test.go b/test/errors/unique/unique_test.go index 1c0bddd7c..62f43948b 100644 --- a/test/errors/unique/unique_test.go +++ b/test/errors/unique/unique_test.go @@ -39,7 +39,7 @@ func TestUniqueConstraintViolation(t *testing.T) { // assert.Equal(t, &ErrUniqueConstraint{ // Field: User.Email.Field(), // }, violation) - assert.Equal(t, User.Email.Field(), violation.Field) + assert.Equal(t, User.Email.Field(), violation.Fields[0]) assert.Equal(t, true, ok) }, diff --git a/test/test.go b/test/test.go index 396c9174c..c052eaeb2 100644 --- a/test/test.go +++ b/test/test.go @@ -3,6 +3,7 @@ package test import ( "context" "fmt" + "github.com/steebchen/prisma-client-go/engine/protocol" "log" "os" "strings" @@ -68,8 +69,8 @@ func Start(t *testing.T, db Database, e engine.Engine, queries []string) string } for _, q := range queries { - var response engine.GQLResponse - payload := engine.GQLRequest{ + var response protocol.GQLResponse + payload := protocol.GQLRequest{ Query: q, Variables: map[string]interface{}{}, }