Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
.DS_Store
pkg/parser/testdata/lotto.graphql
*node_modules*
*vendor*
*vendor*
.vscode
16 changes: 16 additions & 0 deletions .markdownlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Allow for code examples.
no-hard-tabs:
code_blocks: false

# Long lines are OK.
line-length: false

# We get excited!
no-trailing-punctuation:
punctuation: ".,;:"

# Allow inline HTML.
no-inline-html: false

# We lead with badges, it looks right.
first-line-h1: false
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[![GoDoc](https://pkg.go.dev/badge/github.com/wundergraph/graphql-go-tools/v2)](https://pkg.go.dev/github.com/wundergraph/graphql-go-tools/v2)
[![v2-ci](https://github.com/wundergraph/graphql-go-tools/workflows/v2-ci/badge.svg)](https://github.com/wundergraph/graphql-go-tools/actions/workflows/v2.yml)

# GraphQL Router / API Gateway Framework written in Golang

[<p align="center"><img height="auto" src="./assets/logo.png"></p>](https://wundergraph.com/)
[<p align="center"><img height="auto" src="./assets/logo.png" alt="Wundergraph logo"></p>](https://wundergraph.com/)

## We're hiring!

Expand All @@ -19,6 +20,7 @@ If you're looking for a complete ready-to-use Open Source Router for Federation,
have a look at the [Cosmo Router](https://github.com/wundergraph/cosmo) which is based on this library.

Cosmo Router wraps this library and provides a complete solution for Federated GraphQL including the following features:

- [x] Federation Gateway
- [x] OpenTelemetry Metrics & Distributed Tracing
- [x] Prometheus Metrics
Expand All @@ -44,7 +46,6 @@ This repository contains multiple packages joined via [workspace](https://github
| [examples/federation](https://github.com/wundergraph/graphql-go-tools/blob/master/examples/federation/go.mod) | Example implementation of graphql federation gateway. This example is not production ready. For production ready solution please consider using [cosmo router](https://github.com/wundergraph/cosmo/tree/main) | depends on [execution](https://github.com/wundergraph/graphql-go-tools/blob/master/execution/go.mod) package | actual federation gateway example |
| [graphql-go-tools v1](https://github.com/wundergraph/graphql-go-tools/blob/master/go.mod) | Legacy GraphQL engine implementation. This version 1 package is in maintenance mode and accepts only pull requests with critical bug fixes. All new features will be implemented in the version 2 package only. | - | deprecated, maintenance mode |


## Notes

This library is used in production at [WunderGraph](https://wundergraph.com/).
Expand Down Expand Up @@ -298,7 +299,7 @@ func ExampleNormalizeDocument() {
hello: String
foo: Foo
}

type Foo {
bar: String
}
Expand Down Expand Up @@ -534,7 +535,7 @@ func ExamplePlanOperation() {
if err != nil {
panic(err)
}

config := plan.Configuration{
DataSources: []plan.DataSource{
staticDataSource,
Expand Down
2 changes: 2 additions & 0 deletions execution/engine/execution_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ func (e *ExecutionEngine) Execute(ctx context.Context, operation *graphql.Reques
return err
case *plan.SubscriptionResponsePlan:
return e.resolver.ResolveGraphQLSubscription(execContext.resolveContext, p.Response, writer)
case *plan.IncrementalResponsePlan:
return e.resolver.ResolveGraphQLIncrementalResponse(execContext.resolveContext, p.Response, writer)
default:
return errors.New("execution of operation is not possible")
}
Expand Down
7 changes: 5 additions & 2 deletions v2/pkg/engine/plan/analyze_plan_kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ func (p *planKindVisitor) EnterDirective(ref int) {
switch ancestor.Kind {
case ast.NodeKindField:
switch directiveName {
case "defer":
p.hasDeferDirective = true
case "stream":
p.hasStreamDirective = true
}
case ast.NodeKindInlineFragment:
switch directiveName {
case "defer":
p.hasStreamDirective = true
}
}
}

Expand Down
12 changes: 9 additions & 3 deletions v2/pkg/engine/plan/analyze_plan_kind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ func TestAnalyzePlanKind(t *testing.T) {
name
}
primaryFunction
favoriteEpisode @defer
... @defer {
favoriteEpisode
}
}
}`,
"MyQuery",
Expand Down Expand Up @@ -146,7 +148,9 @@ func TestAnalyzePlanKind(t *testing.T) {
name
}
primaryFunction
favoriteEpisode @defer
... @defer {
favoriteEpisode
}
}
}`,
"OperationNameNotExists",
Expand All @@ -167,7 +171,9 @@ func TestAnalyzePlanKind(t *testing.T) {
subscription NewReviews {
newReviews {
id
stars @defer
... @defer {
stars
}
}
}`,
"NewReviews",
Expand Down
14 changes: 14 additions & 0 deletions v2/pkg/engine/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Kind int
const (
SynchronousResponseKind Kind = iota + 1
SubscriptionResponseKind
IncrementalResponseKind
)

type Plan interface {
Expand All @@ -29,6 +30,19 @@ func (_ *SynchronousResponsePlan) PlanKind() Kind {
return SynchronousResponseKind
}

type IncrementalResponsePlan struct {
Response *resolve.GraphQLIncrementalResponse
FlushInterval int64
}

func (s *IncrementalResponsePlan) SetFlushInterval(interval int64) {
s.FlushInterval = interval
}

func (_ *IncrementalResponsePlan) PlanKind() Kind {
return IncrementalResponseKind
}

type SubscriptionResponsePlan struct {
Response *resolve.GraphQLSubscription
FlushInterval int64
Expand Down
170 changes: 165 additions & 5 deletions v2/pkg/engine/plan/planner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,164 @@ func TestPlanner_Plan(t *testing.T) {
}))
})

t.Run("defer planning", func(t *testing.T) {
t.Run("inline fragment", test(testDefinition, `
query WithInlineDefer {
hero {
name
... on Droid @defer {
primaryFunction
favoriteEpisode
}
}
}
`, "WithInlineDefer", &IncrementalResponsePlan{
Response: &resolve.GraphQLIncrementalResponse{
ImmediateResponse: &resolve.GraphQLResponse{
Data: &resolve.Object{
Nullable: false,
Fields: []*resolve.Field{
{
Name: []byte("hero"),
Value: &resolve.Object{
Path: []string{"hero"},
Nullable: true,
TypeName: "Character",
PossibleTypes: map[string]struct{}{"Droid": {}, "Human": {}},
Fields: []*resolve.Field{
{
Name: []byte("name"),
Value: &resolve.String{
Path: []string{"name"},
Nullable: false,
},
},
{
Name: []byte("primaryFunction"),
Value: &resolve.String{
Path: []string{"primaryFunction"},
Nullable: false,
},
OnTypeNames: [][]byte{[]byte("Droid")},
Defer: &resolve.DeferField{},
},
{
Name: []byte("favoriteEpisode"),
Value: &resolve.Enum{
Path: []string{"favoriteEpisode"},
Nullable: true,
TypeName: "Episode",
Values: []string{
"NEWHOPE",
"EMPIRE",
"JEDI",
},
},
OnTypeNames: [][]byte{[]byte("Droid")},
Defer: &resolve.DeferField{},
},
},
},
},
},
Fetches: []resolve.Fetch{
&resolve.SingleFetch{
FetchConfiguration: resolve.FetchConfiguration{
DataSource: &FakeDataSource{&StatefulSource{}},
},
DataSourceIdentifier: []byte("plan.FakeDataSource"),
},
},
},
},
DeferredResponse: nil,
},
}, Configuration{
DisableResolveFieldPositions: true,
DisableIncludeInfo: true,
DataSources: []DataSource{testDefinitionDSConfiguration},
}))

t.Run("fragment spread", test(testDefinition, `
query WithFragmentSpread {
hero {
name
...droid @defer
}
}

fragment droid on Droid {
primaryFunction
favoriteEpisode
}
`, "WithFragmentSpread", &IncrementalResponsePlan{
Response: &resolve.GraphQLIncrementalResponse{
ImmediateResponse: &resolve.GraphQLResponse{
Data: &resolve.Object{
Nullable: false,
Fields: []*resolve.Field{
{
Name: []byte("hero"),
Value: &resolve.Object{
Path: []string{"hero"},
Nullable: true,
TypeName: "Character",
PossibleTypes: map[string]struct{}{"Droid": {}, "Human": {}},
Fields: []*resolve.Field{
{
Name: []byte("name"),
Value: &resolve.String{
Path: []string{"name"},
Nullable: false,
},
},
{
Name: []byte("primaryFunction"),
Value: &resolve.String{
Path: []string{"primaryFunction"},
Nullable: false,
},
OnTypeNames: [][]byte{[]byte("Droid")},
Defer: &resolve.DeferField{},
},
{
Name: []byte("favoriteEpisode"),
Value: &resolve.Enum{
Path: []string{"favoriteEpisode"},
Nullable: true,
TypeName: "Episode",
Values: []string{
"NEWHOPE",
"EMPIRE",
"JEDI",
},
},
OnTypeNames: [][]byte{[]byte("Droid")},
Defer: &resolve.DeferField{},
},
},
},
},
},
Fetches: []resolve.Fetch{
&resolve.SingleFetch{
FetchConfiguration: resolve.FetchConfiguration{
DataSource: &FakeDataSource{&StatefulSource{}},
},
DataSourceIdentifier: []byte("plan.FakeDataSource"),
},
},
},
},
DeferredResponse: nil,
},
}, Configuration{
DisableResolveFieldPositions: true,
DisableIncludeInfo: true,
DataSources: []DataSource{testDefinitionDSConfiguration},
}))
})

t.Run("operation selection", func(t *testing.T) {
cfg := Configuration{
DataSources: []DataSource{testDefinitionDSConfiguration},
Expand Down Expand Up @@ -379,7 +537,7 @@ func TestPlanner_Plan(t *testing.T) {
name
}
}

query MyHero {
hero{
name
Expand All @@ -392,15 +550,15 @@ func TestPlanner_Plan(t *testing.T) {
t.Run("unescape response json", func(t *testing.T) {
schema := `
scalar JSON

schema {
query: Query
}

type Query {
hero: Character!
}

type Character {
info: JSON!
infos: [JSON!]!
Expand Down Expand Up @@ -678,7 +836,9 @@ var testDefinitionDSConfiguration = dsb().

const testDefinition = `

directive @defer on FIELD
directive @defer(label: String if: Boolean! = true) on FRAGMENT_SPREAD | INLINE_FRAGMENT

directive @stream(label: String if: Boolean! = true initialCount: Int = 0) on FIELD

directive @flushInterval(milliSeconds: Int!) on QUERY | SUBSCRIPTION

Expand Down
Loading