Skip to content

Commit

Permalink
feat: Add transformation option to the field configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
bakjos committed Jan 27, 2023
1 parent 12ddf9d commit 6b78285
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 24 deletions.
120 changes: 107 additions & 13 deletions pkg/engine/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"regexp"
"strings"

"github.com/jensneuse/pipeline/pkg/pipe"
"github.com/jensneuse/pipeline/pkg/step"

"github.com/wundergraph/graphql-go-tools/pkg/ast"
"github.com/wundergraph/graphql-go-tools/pkg/astimport"
"github.com/wundergraph/graphql-go-tools/pkg/astvisitor"
Expand Down Expand Up @@ -121,6 +124,8 @@ type FieldConfiguration struct {
// e.g. {"response":"{\"foo\":\"bar\"}"} will be returned as {"foo":"bar"} when path is "response"
// This way, it is possible to resolve a JSON string as part of the response without extra String encoding of the JSON
UnescapeResponseJson bool
// A Template representing the JSON Transformation
Transformation string
}

type ArgumentsConfigurations []ArgumentConfiguration
Expand Down Expand Up @@ -566,12 +571,13 @@ func (v *Visitor) EnterField(ref int) {
}

path := v.resolveFieldPath(ref)
transformation := v.resolveTransformation(ref)
fieldDefinitionType := v.Definition.FieldDefinitionType(fieldDefinition)
bufferID, hasBuffer := v.fieldBuffers[ref]

v.currentField = &resolve.Field{
Name: fieldAliasOrName,
Value: v.resolveFieldValue(ref, fieldDefinitionType, true, path),
Value: v.resolveFieldValue(ref, fieldDefinitionType, true, path, transformation),
HasBuffer: hasBuffer,
BufferID: bufferID,
OnTypeName: v.resolveOnTypeName(),
Expand Down Expand Up @@ -684,7 +690,7 @@ func (v *Visitor) skipField(ref int) bool {
return false
}

func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path []string) resolve.Node {
func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path []string, transformation *pipe.Pipeline) resolve.Node {
ofType := v.Definition.Types[typeRef].OfType

fieldName := v.Operation.FieldNameString(fieldRef)
Expand All @@ -697,14 +703,21 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [

switch v.Definition.Types[typeRef].TypeKind {
case ast.TypeKindNonNull:
return v.resolveFieldValue(fieldRef, ofType, false, path)
return v.resolveFieldValue(fieldRef, ofType, false, path, transformation)
case ast.TypeKindList:
listItem := v.resolveFieldValue(fieldRef, ofType, true, nil)
return &resolve.Array{
listItem := v.resolveFieldValue(fieldRef, ofType, true, nil, transformation)
array := &resolve.Array{
Nullable: nullable,
Path: path,
Item: listItem,
}
if transformation != nil {
return &resolve.Transformation{
InnerValue: array,
Pipeline: transformation,
}
}
return array
case ast.TypeKindNamed:
typeName := v.Definition.ResolveTypeNameString(typeRef)
typeDefinitionNode, ok := v.Definition.Index.FirstNodeByNameStr(typeName)
Expand All @@ -716,44 +729,86 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [
fieldExport := v.resolveFieldExport(fieldRef)
switch typeName {
case "String":
return &resolve.String{
value := &resolve.String{
Path: path,
Nullable: nullable,
Export: fieldExport,
UnescapeResponseJson: unescapeResponseJson,
}
if transformation != nil {
return &resolve.Transformation{
InnerValue: value,
Pipeline: transformation,
}
}
return value
case "Boolean":
return &resolve.Boolean{
value := &resolve.Boolean{
Path: path,
Nullable: nullable,
Export: fieldExport,
}
if transformation != nil {
return &resolve.Transformation{
InnerValue: value,
Pipeline: transformation,
}
}
return value
case "Int":
return &resolve.Integer{
value := &resolve.Integer{
Path: path,
Nullable: nullable,
Export: fieldExport,
}
if transformation != nil {
return &resolve.Transformation{
InnerValue: value,
Pipeline: transformation,
}
}
return value
case "Float":
return &resolve.Float{
value := &resolve.Float{
Path: path,
Nullable: nullable,
Export: fieldExport,
}
if transformation != nil {
return &resolve.Transformation{
InnerValue: value,
Pipeline: transformation,
}
}
return value
default:
return &resolve.String{
value := &resolve.String{
Path: path,
Nullable: nullable,
Export: fieldExport,
UnescapeResponseJson: unescapeResponseJson,
}
if transformation != nil {
return &resolve.Transformation{
InnerValue: value,
Pipeline: transformation,
}
}
return value
}
case ast.NodeKindEnumTypeDefinition:
return &resolve.String{
value := &resolve.String{
Path: path,
Nullable: nullable,
UnescapeResponseJson: unescapeResponseJson,
}
if transformation != nil {
return &resolve.Transformation{
InnerValue: value,
Pipeline: transformation,
}
}
return value
case ast.NodeKindObjectTypeDefinition, ast.NodeKindInterfaceTypeDefinition, ast.NodeKindUnionTypeDefinition:
object := &resolve.Object{
Nullable: nullable,
Expand All @@ -768,12 +823,32 @@ func (v *Visitor) resolveFieldValue(fieldRef, typeRef int, nullable bool, path [
fields: &object.Fields,
})
})
if transformation != nil {
return &resolve.Transformation{
InnerValue: object,
Pipeline: transformation,
}
}
return object
default:
return &resolve.Null{}
value := &resolve.Null{}
if transformation != nil {
return &resolve.Transformation{
InnerValue: value,
Pipeline: transformation,
}
}
return value
}
default:
return &resolve.Null{}
value := &resolve.Null{}
if transformation != nil {
return &resolve.Transformation{
InnerValue: value,
Pipeline: transformation,
}
}
return value
}
}

Expand Down Expand Up @@ -938,6 +1013,25 @@ func (v *Visitor) resolveFieldPath(ref int) []string {
return []string{fieldName}
}

func (v *Visitor) resolveTransformation(ref int) *pipe.Pipeline {
typeName := v.Walker.EnclosingTypeDefinition.NameString(v.Definition)
fieldName := v.Operation.FieldNameUnsafeString(ref)

for i := range v.Config.Fields {
if v.Config.Fields[i].TypeName == typeName && v.Config.Fields[i].FieldName == fieldName {
if len(v.Config.Fields[i].Transformation) > 0 {
step, err := step.NewJSON(v.Config.Fields[i].Transformation)
if err == nil {
return &pipe.Pipeline{Steps: []pipe.Step{step}}
}

}
return nil
}
}
return nil
}

func (v *Visitor) EnterDocument(operation, definition *ast.Document) {
v.Operation, v.Definition = operation, definition
v.fieldConfigs = map[int]*FieldConfiguration{}
Expand Down
54 changes: 43 additions & 11 deletions pkg/engine/resolve/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/buger/jsonparser"
"github.com/cespare/xxhash/v2"
"github.com/jensneuse/pipeline/pkg/pipe"
"github.com/tidwall/gjson"
errors "golang.org/x/xerrors"

Expand Down Expand Up @@ -95,6 +96,7 @@ const (
NodeKindBoolean
NodeKindInteger
NodeKindFloat
NodeKindTransformation

FetchKindSingle FetchKind = iota + 1
FetchKindParallel
Expand Down Expand Up @@ -317,17 +319,18 @@ type SubscriptionDataSource interface {
}

type Resolver struct {
ctx context.Context
dataLoaderEnabled bool
resultSetPool sync.Pool
byteSlicesPool sync.Pool
waitGroupPool sync.Pool
bufPairPool sync.Pool
bufPairSlicePool sync.Pool
errChanPool sync.Pool
hash64Pool sync.Pool
dataloaderFactory *dataLoaderFactory
fetcher *Fetcher
ctx context.Context
dataLoaderEnabled bool
resultSetPool sync.Pool
byteSlicesPool sync.Pool
transformationPool sync.Pool
waitGroupPool sync.Pool
bufPairPool sync.Pool
bufPairSlicePool sync.Pool
errChanPool sync.Pool
hash64Pool sync.Pool
dataloaderFactory *dataLoaderFactory
fetcher *Fetcher
}

type inflightFetch struct {
Expand All @@ -354,6 +357,11 @@ func New(ctx context.Context, fetcher *Fetcher, enableDataLoader bool) *Resolver
return &slice
},
},
transformationPool: sync.Pool{
New: func() interface{} {
return fastbuffer.New()
},
},
waitGroupPool: sync.Pool{
New: func() interface{} {
return &sync.WaitGroup{}
Expand Down Expand Up @@ -416,6 +424,8 @@ func (r *Resolver) resolveNode(ctx *Context, node Node, data []byte, bufPair *Bu
case *EmptyArray:
r.resolveEmptyArray(bufPair.Data)
return
case *Transformation:
return r.resolveTransformation(ctx, n, data, bufPair)
default:
return
}
Expand Down Expand Up @@ -869,6 +879,18 @@ func (r *Resolver) resolveArrayAsynchronous(ctx *Context, array *Array, arrayIte
return
}

func (r *Resolver) resolveTransformation(ctx *Context, t *Transformation, data []byte, transformBuf *BufPair) error {
buffer := r.transformationPool.Get().(*fastbuffer.FastBuffer)
defer func() {
r.transformationPool.Put(buffer)
}()

if err := t.Pipeline.Run(bytes.NewReader(data), buffer); err != nil {
return err
}
return r.resolveNode(ctx, t.InnerValue, buffer.Bytes(), transformBuf)
}

func (r *Resolver) exportField(ctx *Context, export *FieldExport, value []byte) {
if export == nil {
return
Expand Down Expand Up @@ -1358,6 +1380,7 @@ type Field struct {
SkipVariableName string
IncludeDirectiveDefined bool
IncludeVariableName string
Transformation
}

type Position struct {
Expand Down Expand Up @@ -1497,10 +1520,19 @@ type Stream struct {
PatchIndex int
}

type Transformation struct {
InnerValue Node
Pipeline *pipe.Pipeline
}

func (_ *Array) NodeKind() NodeKind {
return NodeKindArray
}

func (_ *Transformation) NodeKind() NodeKind {
return NodeKindTransformation
}

type GraphQLSubscription struct {
Trigger GraphQLSubscriptionTrigger
Response *GraphQLResponse
Expand Down
50 changes: 50 additions & 0 deletions pkg/engine/resolve/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"time"

"github.com/golang/mock/gomock"
"github.com/jensneuse/pipeline/pkg/pipe"
"github.com/jensneuse/pipeline/pkg/step"
"github.com/stretchr/testify/assert"

"github.com/wundergraph/graphql-go-tools/pkg/fastbuffer"
Expand Down Expand Up @@ -1514,6 +1516,54 @@ func TestResolver_ResolveNode(t *testing.T) {
},
}, Context{Context: context.Background()}, `{"id":1,"name":"Jens","pet":{"name":"Woofie"}}`
}))
t.Run("simple transformation", testFn(true, false, func(t *testing.T, ctrl *gomock.Controller) (node Node, ctx Context, expectedOutput string) {
step, err := step.NewJSON("{\"fullName\":\"{{ .firstName }} {{ .lastName }}\"}")
assert.NoError(t, err)
return &Object{
Fetch: &SingleFetch{
BufferId: 0,
DataSource: FakeDataSource(`{"firstName":"John","lastName":"Doe"}`),
},
Fields: []*Field{
{
HasBuffer: true,
BufferID: 0,
Name: []byte("firstName"),
Value: &String{
Path: []string{"firstName"},
},
},
{
HasBuffer: true,
BufferID: 0,
Name: []byte("lastName"),
Value: &String{
Path: []string{"lastName"},
},
},
{
HasBuffer: true,
BufferID: 0,
Name: []byte("name"),
Value: &Transformation{
InnerValue: &Object{
Fields: []*Field{
{
BufferID: 0,
HasBuffer: true,
Name: []byte("fullName"),
Value: &String{
Path: []string{"fullName"},
},
},
},
},
Pipeline: &pipe.Pipeline{Steps: []pipe.Step{step}},
},
},
},
}, Context{Context: context.Background()}, `{"firstName":"John","lastName":"Doe","name":{"fullName":"John Doe"}}`
}))
t.Run("with unescape json enabled", func(t *testing.T) {
t.Run("json object within a string", testFn(false, false, func(t *testing.T, ctrl *gomock.Controller) (node Node, ctx Context, expectedOutput string) {
return &Object{
Expand Down
Loading

0 comments on commit 6b78285

Please sign in to comment.