Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add client extensions #686

Merged
merged 1 commit into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ func (c *SubscriptionClient) generateHandlerIDHash(ctx *resolve.Context, options
}
}
}
if options.Body.Extensions != nil {
if _, err := xxh.Write(options.Body.Extensions); err != nil {
return 0, err
}
}
return xxh.Sum64(), nil
}

Expand Down Expand Up @@ -254,6 +259,13 @@ func (c *SubscriptionClient) newWSConnectionHandler(reqCtx context.Context, opti
return nil, err
}

if options.Body.Extensions != nil {
connectionInitMessage, err = jsonparser.Set(connectionInitMessage, options.Body.Extensions, "payload", "extensions")
if err != nil {
return nil, err
}
}

// init + ack
err = conn.Write(reqCtx, websocket.MessageText, connectionInitMessage)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions v2/pkg/engine/resolve/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Context struct {
Request Request
RenameTypeNames []RenameTypeName
RequestTracingOptions RequestTraceOptions
Extensions []byte
}

type Request struct {
Expand Down
10 changes: 8 additions & 2 deletions v2/pkg/engine/resolve/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,13 @@ WithNextItem:
return nil
}

func (l *Loader) executeSourceLoad(ctx context.Context, disallowSingleFlight bool, source DataSource, input []byte, out io.Writer, trace *DataSourceLoadTrace) error {
func (l *Loader) executeSourceLoad(ctx context.Context, disallowSingleFlight bool, source DataSource, input []byte, out io.Writer, trace *DataSourceLoadTrace) (err error) {
if l.ctx.Extensions != nil {
input, err = jsonparser.Set(input, l.ctx.Extensions, "body", "extensions")
if err != nil {
return errors.WithStack(err)
}
}
if l.traceOptions.Enable {
trace.Path = l.renderPath()
if !l.traceOptions.ExcludeInput {
Expand Down Expand Up @@ -825,7 +831,7 @@ func (l *Loader) executeSourceLoad(ctx context.Context, disallowSingleFlight boo
}
keyGen := pool.Hash64.Get()
defer pool.Hash64.Put(keyGen)
_, err := keyGen.Write(input)
_, err = keyGen.Write(input)
if err != nil {
return errors.WithStack(err)
}
Expand Down
308 changes: 291 additions & 17 deletions v2/pkg/engine/resolve/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/wundergraph/graphql-go-tools/v2/pkg/astjson"
)

func TestV2Loader_LoadGraphQLResponseData(t *testing.T) {
func TestLoader_LoadGraphQLResponseData(t *testing.T) {
ctrl := gomock.NewController(t)
productsService := mockedDS(t, ctrl,
`{"method":"POST","url":"http://products","body":{"query":"query{topProducts{name __typename upc}}"}}`,
Expand Down Expand Up @@ -299,7 +299,296 @@ func TestV2Loader_LoadGraphQLResponseData(t *testing.T) {
assert.Equal(t, expected, out.String())
}

func BenchmarkV2Loader_LoadGraphQLResponseData(b *testing.B) {
func TestLoader_LoadGraphQLResponseDataWithExtensions(t *testing.T) {
ctrl := gomock.NewController(t)
productsService := mockedDS(t, ctrl,
`{"method":"POST","url":"http://products","body":{"query":"query{topProducts{name __typename upc}}","extensions":{"foo":"bar"}}}`,
`{"topProducts":[{"name":"Table","__typename":"Product","upc":"1"},{"name":"Couch","__typename":"Product","upc":"2"},{"name":"Chair","__typename":"Product","upc":"3"}]}`)

reviewsService := mockedDS(t, ctrl,
`{"method":"POST","url":"http://reviews","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on Product {reviews {body author {__typename id}}}}}","variables":{"representations":[{"__typename":"Product","upc":"1"},{"__typename":"Product","upc":"2"},{"__typename":"Product","upc":"3"}]},"extensions":{"foo":"bar"}}}`,
`{"_entities":[{"__typename":"Product","reviews":[{"body":"Love Table!","author":{"__typename":"User","id":"1"}},{"body":"Prefer other Table.","author":{"__typename":"User","id":"2"}}]},{"__typename":"Product","reviews":[{"body":"Couch Too expensive.","author":{"__typename":"User","id":"1"}}]},{"__typename":"Product","reviews":[{"body":"Chair Could be better.","author":{"__typename":"User","id":"2"}}]}]}`)

stockService := mockedDS(t, ctrl,
`{"method":"POST","url":"http://stock","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on Product {stock}}}","variables":{"representations":[{"__typename":"Product","upc":"1"},{"__typename":"Product","upc":"2"},{"__typename":"Product","upc":"3"}]},"extensions":{"foo":"bar"}}}`,
`{"_entities":[{"stock":8},{"stock":2},{"stock":5}]}`)

usersService := mockedDS(t, ctrl,
`{"method":"POST","url":"http://users","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {name}}}","variables":{"representations":[{"__typename":"User","id":"1"},{"__typename":"User","id":"2"}]},"extensions":{"foo":"bar"}}}`,
`{"_entities":[{"name":"user-1"},{"name":"user-2"}]}`)
response := &GraphQLResponse{
Data: &Object{
Fetch: &SingleFetch{
InputTemplate: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`{"method":"POST","url":"http://products","body":{"query":"query{topProducts{name __typename upc}}"}}`),
SegmentType: StaticSegmentType,
},
},
},
FetchConfiguration: FetchConfiguration{
DataSource: productsService,
PostProcessing: PostProcessingConfiguration{
SelectResponseDataPath: []string{"data"},
},
},
},
Fields: []*Field{
{
Name: []byte("topProducts"),
Value: &Array{
Path: []string{"topProducts"},
Item: &Object{
Fetch: &ParallelFetch{
Fetches: []Fetch{
&BatchEntityFetch{
Input: BatchInput{
Header: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`{"method":"POST","url":"http://reviews","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on Product {reviews {body author {__typename id}}}}}","variables":{"representations":[`),
SegmentType: StaticSegmentType,
},
},
},
Items: []InputTemplate{
{
Segments: []TemplateSegment{
{
SegmentType: VariableSegmentType,
VariableKind: ResolvableObjectVariableKind,
Renderer: NewGraphQLVariableResolveRenderer(&Object{
Fields: []*Field{
{
Name: []byte("__typename"),
Value: &String{
Path: []string{"__typename"},
},
},
{
Name: []byte("upc"),
Value: &String{
Path: []string{"upc"},
},
},
},
}),
},
},
},
},
Separator: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`,`),
SegmentType: StaticSegmentType,
},
},
},
Footer: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`]}}}`),
SegmentType: StaticSegmentType,
},
},
},
},
DataSource: reviewsService,
PostProcessing: PostProcessingConfiguration{
SelectResponseDataPath: []string{"data", "_entities"},
},
},
&BatchEntityFetch{
Input: BatchInput{
Header: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`{"method":"POST","url":"http://stock","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on Product {stock}}}","variables":{"representations":[`),
SegmentType: StaticSegmentType,
},
},
},
Items: []InputTemplate{
{
Segments: []TemplateSegment{
{
SegmentType: VariableSegmentType,
VariableKind: ResolvableObjectVariableKind,
Renderer: NewGraphQLVariableResolveRenderer(&Object{
Fields: []*Field{
{
Name: []byte("__typename"),
Value: &String{
Path: []string{"__typename"},
},
},
{
Name: []byte("upc"),
Value: &String{
Path: []string{"upc"},
},
},
},
}),
},
},
},
},
Separator: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`,`),
SegmentType: StaticSegmentType,
},
},
},
Footer: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`]}}}`),
SegmentType: StaticSegmentType,
},
},
},
},
DataSource: stockService,
PostProcessing: PostProcessingConfiguration{
SelectResponseDataPath: []string{"data", "_entities"},
},
},
},
},
Fields: []*Field{
{
Name: []byte("name"),
Value: &String{
Path: []string{"name"},
},
},
{
Name: []byte("stock"),
Value: &Integer{
Path: []string{"stock"},
},
},
{
Name: []byte("reviews"),
Value: &Array{
Path: []string{"reviews"},
Item: &Object{
Fields: []*Field{
{
Name: []byte("body"),
Value: &String{
Path: []string{"body"},
},
},
{
Name: []byte("author"),
Value: &Object{
Path: []string{"author"},
Fetch: &BatchEntityFetch{
Input: BatchInput{
Header: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`{"method":"POST","url":"http://users","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {name}}}","variables":{"representations":[`),
SegmentType: StaticSegmentType,
},
},
},
Items: []InputTemplate{
{
Segments: []TemplateSegment{
{
SegmentType: VariableSegmentType,
VariableKind: ResolvableObjectVariableKind,
Renderer: NewGraphQLVariableResolveRenderer(&Object{
Fields: []*Field{
{
Name: []byte("__typename"),
Value: &String{
Path: []string{"__typename"},
},
},
{
Name: []byte("id"),
Value: &String{
Path: []string{"id"},
},
},
},
}),
},
},
},
},
Separator: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`,`),
SegmentType: StaticSegmentType,
},
},
},
Footer: InputTemplate{
Segments: []TemplateSegment{
{
Data: []byte(`]}}}`),
SegmentType: StaticSegmentType,
},
},
},
},
DataSource: usersService,
PostProcessing: PostProcessingConfiguration{
SelectResponseDataPath: []string{"data", "_entities"},
},
},
Fields: []*Field{
{
Name: []byte("name"),
Value: &String{
Path: []string{"name"},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
}
ctx := &Context{
ctx: context.Background(),
Extensions: []byte(`{"foo":"bar"}`),
}
resolvable := &Resolvable{
storage: &astjson.JSON{},
}
loader := &Loader{}
err := resolvable.Init(ctx, nil, ast.OperationTypeQuery)
assert.NoError(t, err)
err = loader.LoadGraphQLResponseData(ctx, response, resolvable)
assert.NoError(t, err)
ctrl.Finish()
out := &bytes.Buffer{}
err = resolvable.storage.PrintNode(resolvable.storage.Nodes[resolvable.storage.RootNode], out)
assert.NoError(t, err)
expected := `{"errors":[],"data":{"topProducts":[{"name":"Table","__typename":"Product","upc":"1","reviews":[{"body":"Love Table!","author":{"__typename":"User","id":"1","name":"user-1"}},{"body":"Prefer other Table.","author":{"__typename":"User","id":"2","name":"user-2"}}],"stock":8},{"name":"Couch","__typename":"Product","upc":"2","reviews":[{"body":"Couch Too expensive.","author":{"__typename":"User","id":"1","name":"user-1"}}],"stock":2},{"name":"Chair","__typename":"Product","upc":"3","reviews":[{"body":"Chair Could be better.","author":{"__typename":"User","id":"2","name":"user-2"}}],"stock":5}]}}`
assert.Equal(t, expected, out.String())
}

func BenchmarkLoader_LoadGraphQLResponseData(b *testing.B) {

productsService := FakeDataSource(`{"data":{"topProducts":[{"name":"Table","__typename":"Product","upc":"1"},{"name":"Couch","__typename":"Product","upc":"2"},{"name":"Chair","__typename":"Product","upc":"3"}]}}`)
reviewsService := FakeDataSource(`{"data":{"_entities":[{"__typename":"Product","reviews":[{"body":"Love Table!","author":{"__typename":"User","id":"1"}},{"body":"Prefer other Table.","author":{"__typename":"User","id":"2"}}]},{"__typename":"Product","reviews":[{"body":"Couch Too expensive.","author":{"__typename":"User","id":"1"}}]},{"__typename":"Product","reviews":[{"body":"Chair Could be better.","author":{"__typename":"User","id":"2"}}]}]}}`)
Expand Down Expand Up @@ -591,18 +880,3 @@ func BenchmarkV2Loader_LoadGraphQLResponseData(b *testing.B) {
}
}
}

var (
DefaultPostProcessingConfiguration = PostProcessingConfiguration{
SelectResponseDataPath: []string{"data"},
SelectResponseErrorsPath: []string{"errors"},
}
EntitiesPostProcessingConfiguration = PostProcessingConfiguration{
SelectResponseDataPath: []string{"data", "_entities"},
SelectResponseErrorsPath: []string{"errors"},
}
SingleEntityPostProcessingConfiguration = PostProcessingConfiguration{
SelectResponseDataPath: []string{"data", "_entities", "[0]"},
SelectResponseErrorsPath: []string{"errors"},
}
)
Loading
Loading