Skip to content

Commit

Permalink
feat(GraphQL): GraphQL now has lambda resolvers (#6574)
Browse files Browse the repository at this point in the history
This PR adds `@lambda` directive in GraphQL, using which one can call Custom JavaScript resolvers. Now, alpha has a flag called `--graphql_lambda_url` which is used to set the URL of the lambda server. All the `@lambda` fields will be resolved through the lambda functions implemented on the given lambda server.

RFC: https://discuss.dgraph.io/t/implement-custom-js-resolvers-in-graphql/9361
Lambda server: https://github.com/dgraph-io/dgraph-lambda

(cherry picked from commit 2f3d7f4)

# Conflicts:
#	graphql/e2e/common/common.go
#	graphql/e2e/directives/docker-compose.yml
#	graphql/e2e/directives/schema.graphql
#	graphql/e2e/normal/docker-compose.yml
#	graphql/e2e/normal/schema.graphql
#	graphql/e2e/schema/generatedSchema.graphql
#	graphql/schema/gqlschema.go
#	graphql/schema/testdata/schemagen/output/authorization.graphql
#	graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql
#	graphql/schema/testdata/schemagen/output/custom-mutation.graphql
#	graphql/schema/testdata/schemagen/output/custom-nested-types.graphql
#	graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql
#	graphql/schema/testdata/schemagen/output/custom-query-not-dgraph-type.graphql
#	graphql/schema/testdata/schemagen/output/custom-query-with-dgraph-type.graphql
#	graphql/schema/testdata/schemagen/output/deprecated.graphql
#	graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-on-concrete-type-with-interfaces.graphql
#	graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-with-interfaces.graphql
#	graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql
#	graphql/schema/testdata/schemagen/output/field-with-reverse-predicate-in-dgraph-directive.graphql
#	graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql
#	graphql/schema/testdata/schemagen/output/hasInverse-with-interface-having-directive.graphql
#	graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql
#	graphql/schema/testdata/schemagen/output/hasInverse-with-type-having-directive.graphql
#	graphql/schema/testdata/schemagen/output/hasInverse.graphql
#	graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql
#	graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql
#	graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql
#	graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql
#	graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql
#	graphql/schema/testdata/schemagen/output/interfaces-with-types-and-password.graphql
#	graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql
#	graphql/schema/testdata/schemagen/output/no-id-field-with-searchables.graphql
#	graphql/schema/testdata/schemagen/output/no-id-field.graphql
#	graphql/schema/testdata/schemagen/output/password-type.graphql
#	graphql/schema/testdata/schemagen/output/searchables-references.graphql
#	graphql/schema/testdata/schemagen/output/searchables.graphql
#	graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql
#	graphql/schema/testdata/schemagen/output/single-type.graphql
#	graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql
#	graphql/schema/testdata/schemagen/output/type-reference.graphql
#	graphql/schema/testdata/schemagen/output/type-with-arguments-on-field.graphql
#	graphql/schema/testdata/schemagen/output/type-with-custom-field-on-dgraph-type.graphql
#	graphql/schema/testdata/schemagen/output/type-with-custom-fields-on-remote-type.graphql
#	graphql/schema/testdata/schemagen/output/type-without-orderables.graphql
  • Loading branch information
abhimanyusinghgaur committed Oct 15, 2020
1 parent 158680a commit c01690f
Show file tree
Hide file tree
Showing 66 changed files with 1,042 additions and 94 deletions.
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 @@ -212,6 +213,8 @@ they form a Raft group and provide synchronous replication.
flag.Bool("ludicrous_mode", false, "Run alpha in ludicrous mode")
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.Int64("cache_mb", 0, "Total size of cache (in MB) to be used in alpha.")
Expand Down Expand Up @@ -725,6 +728,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 @@ -252,6 +252,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) {
if metainfo.Algo == "" {
return nil, fmt.Errorf(
Expand Down
5 changes: 5 additions & 0 deletions graphql/e2e/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,11 @@ func RunAll(t *testing.T) {
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
24 changes: 22 additions & 2 deletions 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 --lru_mb=1024 --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 --lru_mb=1024 --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 @@ -66,4 +69,21 @@ services:
labels:
cluster: admintest
service: alphaAdmin
command: /gobin/dgraph alpha --lru_mb=1024 --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
command: /gobin/dgraph alpha --lru_mb=1024 --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
13 changes: 12 additions & 1 deletion 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 @@ -89,6 +91,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 @@ -177,4 +180,12 @@ type Person1 {
id: ID!
name: String!
friends: [Person1] @hasInverse(field: friends)
}
}

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

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

0 comments on commit c01690f

Please sign in to comment.