Skip to content

feat(GraphQL): GraphQL now has lambda resolvers #6574

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

Merged
merged 16 commits into from
Oct 6, 2020
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
16 changes: 16 additions & 0 deletions dgraph/cmd/alpha/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"net"
"net/http"
_ "net/http/pprof" // http profiler
"net/url"
"os"
"os/signal"
"strings"
Expand Down Expand Up @@ -195,6 +196,8 @@ they form a Raft group and provide synchronous replication.

flag.Bool("graphql_extensions", true, "Set to false if extensions not required in GraphQL response body")
flag.Duration("graphql_poll_interval", time.Second, "polling interval for graphql subscription.")
flag.String("graphql_lambda_url", "",
"URL of lambda server that implements custom GraphQL JavaScript resolvers")

// Cache flags
flag.String("cache_percentage", "0,65,35,0",
Expand Down Expand Up @@ -688,6 +691,19 @@ func run() {
x.Config.PollInterval = Alpha.Conf.GetDuration("graphql_poll_interval")
x.Config.GraphqlExtension = Alpha.Conf.GetBool("graphql_extensions")
x.Config.GraphqlDebug = Alpha.Conf.GetBool("graphql_debug")
x.Config.GraphqlLambdaUrl = Alpha.Conf.GetString("graphql_lambda_url")
if x.Config.GraphqlLambdaUrl != "" {
graphqlLambdaUrl, err := url.Parse(x.Config.GraphqlLambdaUrl)
if err != nil {
glog.Errorf("unable to parse graphql_lambda_url: %v", err)
return
}
if !graphqlLambdaUrl.IsAbs() {
glog.Errorf("expecting graphql_lambda_url to be an absolute URL, got: %s",
graphqlLambdaUrl.String())
return
}
}

x.PrintVersion()
glog.Infof("x.Config: %+v", x.Config)
Expand Down
12 changes: 12 additions & 0 deletions graphql/authorization/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,18 @@ func ExtractCustomClaims(ctx context.Context) (*CustomClaims, error) {
return validateJWTCustomClaims(jwtToken[0])
}

func GetJwtToken(ctx context.Context) string {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ""
}
jwtToken := md.Get(string(AuthJwtCtxKey))
if len(jwtToken) != 1 {
return ""
}
return jwtToken[0]
}

func validateJWTCustomClaims(jwtStr string) (*CustomClaims, error) {
jwkURL := authMeta.jwkURL()

Expand Down
6 changes: 6 additions & 0 deletions graphql/e2e/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,12 @@ func RunAll(t *testing.T) {
t.Run("fragment in query", fragmentInQuery)
t.Run("fragment in query on Interface", fragmentInQueryOnInterface)
t.Run("fragment in query on Object", fragmentInQueryOnObject)

// lambda tests
t.Run("lambda on type field", lambdaOnTypeField)
t.Run("lambda on interface field", lambdaOnInterfaceField)
t.Run("lambda on query using dql", lambdaOnQueryUsingDql)
t.Run("lambda on mutation using graphql", lambdaOnMutationUsingGraphQL)
}

// RunCorsTest test all cors related tests.
Expand Down
197 changes: 197 additions & 0 deletions graphql/e2e/common/lambda.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright 2020 Dgraph Labs, Inc. and Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package common

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"

"github.com/dgraph-io/dgraph/testutil"
)

func lambdaOnTypeField(t *testing.T) {
query := `
query {
queryAuthor {
name
bio
rank
}
}`
params := &GraphQLParams{Query: query}
resp := params.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, resp)

expectedResponse := `{
"queryAuthor": [
{
"name":"Three Author",
"bio":"My name is Three Author and I was born on 2001-01-01T00:00:00Z.",
"rank":1
},
{
"name":"Ann Author",
"bio":"My name is Ann Author and I was born on 2000-01-01T00:00:00Z.",
"rank":3
},
{
"name":"Ann Other Author",
"bio":"My name is Ann Other Author and I was born on 1988-01-01T00:00:00Z.",
"rank":2
}
]
}`
testutil.CompareJSON(t, expectedResponse, string(resp.Data))
}

func lambdaOnInterfaceField(t *testing.T) {
starship := addStarship(t)
humanID := addHuman(t, starship.ID)
droidID := addDroid(t)

// when querying bio on Character (interface) we should get the bio constructed by the lambda
// registered on Character.bio
query := `
query {
queryCharacter {
name
bio
}
}`
params := &GraphQLParams{Query: query}
resp := params.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, resp)

expectedResponse := `{
"queryCharacter": [
{
"name":"Han",
"bio":"My name is Han."
},
{
"name":"R2-D2",
"bio":"My name is R2-D2."
}
]
}`
testutil.CompareJSON(t, expectedResponse, string(resp.Data))

// TODO: this should work. At present there is a bug with @custom on interface field resolved
// through a fragment on one of its types. We need to fix that first, then uncomment this test.

// when querying bio on Human & Droid (type) we should get the bio constructed by the lambda
// registered on Human.bio and Droid.bio respectively
/*query = `
query {
queryCharacter {
name
... on Human {
bio
}
... on Droid {
bio
}
}
}`
params = &GraphQLParams{Query: query}
resp = params.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, resp)

expectedResponse = `{
"queryCharacter": [
{
"name":"Han",
"bio":"My name is Han. I have 10 credits."
},
{
"name":"R2-D2",
"bio":"My name is R2-D2. My primary function is Robot."
}
]
}`
testutil.CompareJSON(t, expectedResponse, string(resp.Data))*/

// cleanup
cleanupStarwars(t, starship.ID, humanID, droidID)
}

func lambdaOnQueryUsingDql(t *testing.T) {
query := `
query {
authorsByName(name: "Ann Author") {
name
dob
reputation
}
}`
params := &GraphQLParams{Query: query}
resp := params.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, resp)

expectedResponse := `{
"authorsByName": [
{
"name":"Ann Author",
"dob":"2000-01-01T00:00:00Z",
"reputation":6.6
}
]
}`
testutil.CompareJSON(t, expectedResponse, string(resp.Data))
}

func lambdaOnMutationUsingGraphQL(t *testing.T) {
// first, add the author using @lambda
query := `
mutation {
newAuthor(name: "Lambda")
}`
params := &GraphQLParams{Query: query}
resp := params.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, resp)

// let's get the author ID of the newly added author as returned by lambda
var addResp struct {
AuthorID string `json:"newAuthor"`
}
require.NoError(t, json.Unmarshal(resp.Data, &addResp))

// now, lets query the same author and verify that its reputation was set as 3.0 by lambda func
query = `
query ($id: ID!){
getAuthor(id: $id) {
name
reputation
}
}`
params = &GraphQLParams{Query: query, Variables: map[string]interface{}{"id": addResp.AuthorID}}
resp = params.ExecuteAsPost(t, GraphqlURL)
RequireNoGQLErrors(t, resp)

expectedResponse := `{
"getAuthor": {
"name":"Lambda",
"reputation":3.0
}
}`
testutil.CompareJSON(t, expectedResponse, string(resp.Data))

// cleanup
deleteAuthors(t, []string{addResp.AuthorID}, nil)
}
8 changes: 8 additions & 0 deletions graphql/e2e/common/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ const (
{
"name": "posts",
"description": ""
},
{
"name": "bio",
"description": ""
},
{
"name": "rank",
"description": ""
}
],
"enumValues":[]
Expand Down
5 changes: 5 additions & 0 deletions graphql/e2e/custom_logic/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ func compareHeaders(headers map[string][]string, actual http.Header) error {
if headers == nil {
return nil
}
// unless some other content-type was expected, always make sure we get JSON as content-type.
if _, ok := headers["Content-Type"]; !ok {
headers["Content-Type"] = []string{"application/json"}
}

actualHeaderLen := len(actual)
expectedHeaderLen := len(headers)
if actualHeaderLen != expectedHeaderLen {
Expand Down
4 changes: 2 additions & 2 deletions graphql/e2e/custom_logic/custom_logic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func TestCustomQueryShouldForwardHeaders(t *testing.T) {
}

result := params.ExecuteAsPost(t, alphaURL)
require.Nil(t, result.Errors)
require.Nilf(t, result.Errors, "%s", result.Errors)
expected := `{"verifyHeaders":[{"id":"0x3","name":"Star Wars"}]}`
require.Equal(t, expected, string(result.Data))
}
Expand Down Expand Up @@ -269,7 +269,7 @@ func TestCustomNameForwardHeaders(t *testing.T) {
}

result := params.ExecuteAsPost(t, alphaURL)
require.Nil(t, result.Errors)
require.Nilf(t, result.Errors, "%s", result.Errors)
expected := `{"verifyHeaders":[{"id":"0x3","name":"Star Wars"}]}`
require.Equal(t, expected, string(result.Data))
}
Expand Down
5 changes: 5 additions & 0 deletions graphql/e2e/directives/dgraph_directives_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"os"
"testing"

"github.com/dgraph-io/dgraph/x"

"github.com/dgraph-io/dgraph/graphql/e2e/common"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -54,6 +56,9 @@ func TestMain(m *testing.M) {
panic(errors.Wrapf(err, "Unable to read file %s.", jsonFile))
}

// set up the lambda url for unit tests
x.Config.GraphqlLambdaUrl = "http://localhost:8086/graphql-worker"

common.BootstrapServer(schema, data)

os.Exit(m.Run())
Expand Down
22 changes: 21 additions & 1 deletion graphql/e2e/directives/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ services:
labels:
cluster: test
service: alpha1
command: /gobin/dgraph alpha --zero=zero1:5180 -o 100 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --whitelist 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 --my=alpha1:7180
command: /gobin/dgraph alpha --zero=zero1:5180 -o 100 --expose_trace --trace 1.0
--profile_mode block --block_rate 10 --logtostderr -v=2
--whitelist 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 --my=alpha1:7180
--graphql_lambda_url=http://lambda:8686/graphql-worker

zeroAdmin:
image: dgraph/dgraph:latest
Expand Down Expand Up @@ -67,3 +70,20 @@ services:
cluster: admintest
service: alphaAdmin
command: /gobin/dgraph alpha --zero=zeroAdmin:5280 -o 200 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --whitelist 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 --my=alphaAdmin:7280

lambda:
image: dgraph/dgraph-lambda:latest
container_name: lambda
labels:
cluster: test
ports:
- 8686:8686
depends_on:
- alpha
environment:
DGRAPH_URL: http://alpha:8180
volumes:
- type: bind
source: ./script.js
target: /app/script.js
read_only: true
11 changes: 11 additions & 0 deletions graphql/e2e/directives/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type Author @dgraph(type: "test.dgraph.author") {
reputation: Float @search
country: Country
posts: [Post!] @hasInverse(field: author)
bio: String @lambda
rank: Int @lambda
}

type Post @dgraph(type: "myPost") {
Expand Down Expand Up @@ -90,6 +92,7 @@ interface Character @dgraph(type: "performance.character") {
id: ID!
name: String! @search(by: [exact])
appearsIn: [Episode!] @search @dgraph(pred: "appears_in")
bio: String @lambda
}

type Human implements Character & Employee {
Expand Down Expand Up @@ -184,3 +187,11 @@ type Person1 {
name: String!
friends: [Person1] @hasInverse(field: friends)
}

type Query {
authorsByName(name: String!): [Author] @lambda
}

type Mutation {
newAuthor(name: String!): ID! @lambda
}
Loading