Skip to content

Commit

Permalink
use user facing errors, refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
steebchen committed Oct 28, 2023
1 parent 9de5f39 commit 820d2a4
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 46 deletions.
5 changes: 2 additions & 3 deletions engine/mock/do.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
29 changes: 24 additions & 5 deletions engine/protocol.go → engine/protocol/protocol.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package engine
package protocol

import (
"encoding/json"
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion engine/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/steebchen/prisma-client-go/engine/protocol"
"net/http"
"net/url"
"path"
Expand Down Expand Up @@ -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)
}
Expand Down
16 changes: 11 additions & 5 deletions engine/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/steebchen/prisma-client-go/engine/protocol"
"net/http"
"time"

Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion runtime/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/steebchen/prisma-client-go/engine/protocol"
"strings"
"time"

Expand Down Expand Up @@ -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{}{},
}
Expand Down
11 changes: 6 additions & 5 deletions runtime/transaction/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{}{},
}
Expand All @@ -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 {
Expand All @@ -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,
}
Expand Down
52 changes: 29 additions & 23 deletions runtime/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
//
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion test/errors/unique/unique_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand Down
5 changes: 3 additions & 2 deletions test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package test
import (
"context"
"fmt"
"github.com/steebchen/prisma-client-go/engine/protocol"
"log"
"os"
"strings"
Expand Down Expand Up @@ -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{}{},
}
Expand Down

0 comments on commit 820d2a4

Please sign in to comment.