From c01690fcb941ab8d5c21183143b4320f006c2d4c Mon Sep 17 00:00:00 2001 From: Abhimanyu Singh Gaur <12651351+abhimanyusinghgaur@users.noreply.github.com> Date: Tue, 6 Oct 2020 16:07:15 +0530 Subject: [PATCH] feat(GraphQL): GraphQL now has lambda resolvers (#6574) 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 2f3d7f4fafc24255cb9e5706c066596b40e28569) # 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 --- dgraph/cmd/alpha/run.go | 16 ++ graphql/authorization/auth.go | 12 + graphql/e2e/common/common.go | 5 + graphql/e2e/common/lambda.go | 197 ++++++++++++++ graphql/e2e/common/schema.go | 8 + graphql/e2e/custom_logic/cmd/main.go | 5 + graphql/e2e/custom_logic/custom_logic_test.go | 4 +- .../e2e/directives/dgraph_directives_test.go | 5 + graphql/e2e/directives/docker-compose.yml | 24 +- graphql/e2e/directives/schema.graphql | 13 +- graphql/e2e/directives/script.js | 51 ++++ graphql/e2e/normal/docker-compose.yml | 24 +- graphql/e2e/normal/normal_test.go | 5 + graphql/e2e/normal/schema.graphql | 13 +- graphql/e2e/normal/script.js | 51 ++++ graphql/e2e/schema/generatedSchema.graphql | 1 + graphql/resolve/custom_mutation_test.yaml | 4 +- graphql/resolve/custom_query_test.yaml | 3 +- graphql/resolve/resolver.go | 42 ++- graphql/schema/dgraph_schemagen_test.yml | 19 +- graphql/schema/gqlschema.go | 50 ++-- graphql/schema/gqlschema_test.yml | 28 +- graphql/schema/rules.go | 67 +++-- graphql/schema/schemagen.go | 2 +- graphql/schema/schemagen_test.go | 8 + .../schemagen/input/lambda-directive.graphql | 14 + .../schemagen/output/authorization.graphql | 1 + .../output/comments-and-descriptions.graphql | 1 + .../schemagen/output/custom-mutation.graphql | 1 + .../output/custom-nested-types.graphql | 1 + .../output/custom-query-mixed-types.graphql | 1 + .../custom-query-not-dgraph-type.graphql | 1 + .../custom-query-with-dgraph-type.graphql | 1 + .../schemagen/output/deprecated.graphql | 1 + ...e-on-concrete-type-with-interfaces.graphql | 1 + ...-reverse-directive-with-interfaces.graphql | 1 + .../output/field-with-id-directive.graphql | 1 + ...erse-predicate-in-dgraph-directive.graphql | 1 + .../filter-cleanSchema-directLink.graphql | 1 + ...se-with-interface-having-directive.graphql | 1 + .../output/hasInverse-with-interface.graphql | 1 + ...Inverse-with-type-having-directive.graphql | 1 + .../schemagen/output/hasInverse.graphql | 1 + .../hasInverse_withSubscription.graphql | 1 + .../ignore-unsupported-directive.graphql | 1 + .../output/interface-with-dgraph-pred.graphql | 1 + .../interface-with-id-directive.graphql | 1 + .../output/interface-with-no-ids.graphql | 1 + ...interfaces-with-types-and-password.graphql | 1 + .../output/interfaces-with-types.graphql | 1 + .../schemagen/output/lambda-directive.graphql | 245 ++++++++++++++++++ .../no-id-field-with-searchables.graphql | 1 + .../schemagen/output/no-id-field.graphql | 1 + .../schemagen/output/password-type.graphql | 1 + .../output/searchables-references.graphql | 1 + .../schemagen/output/searchables.graphql | 1 + .../output/single-type-with-enum.graphql | 1 + .../schemagen/output/single-type.graphql | 1 + ...ype-implements-multiple-interfaces.graphql | 1 + .../schemagen/output/type-reference.graphql | 1 + .../type-with-arguments-on-field.graphql | 1 + ...e-with-custom-field-on-dgraph-type.graphql | 2 +- ...-with-custom-fields-on-remote-type.graphql | 2 +- .../output/type-without-orderables.graphql | 1 + graphql/schema/wrappers.go | 179 ++++++++++++- x/config.go | 2 + 66 files changed, 1042 insertions(+), 94 deletions(-) create mode 100644 graphql/e2e/common/lambda.go create mode 100644 graphql/e2e/directives/script.js create mode 100644 graphql/e2e/normal/script.js create mode 100644 graphql/schema/testdata/schemagen/input/lambda-directive.graphql create mode 100644 graphql/schema/testdata/schemagen/output/lambda-directive.graphql diff --git a/dgraph/cmd/alpha/run.go b/dgraph/cmd/alpha/run.go index 3eaf3ab3a78..66bbb5c222b 100644 --- a/dgraph/cmd/alpha/run.go +++ b/dgraph/cmd/alpha/run.go @@ -27,6 +27,7 @@ import ( "net" "net/http" _ "net/http/pprof" // http profiler + "net/url" "os" "os/signal" "strings" @@ -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.") @@ -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) diff --git a/graphql/authorization/auth.go b/graphql/authorization/auth.go index 849b54b45d1..3fb2bcc5fd8 100644 --- a/graphql/authorization/auth.go +++ b/graphql/authorization/auth.go @@ -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( diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index fb925115cd1..d42b512130e 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -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. diff --git a/graphql/e2e/common/lambda.go b/graphql/e2e/common/lambda.go new file mode 100644 index 00000000000..ee9f1d3210a --- /dev/null +++ b/graphql/e2e/common/lambda.go @@ -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) +} diff --git a/graphql/e2e/common/schema.go b/graphql/e2e/common/schema.go index ebccc782792..5cd6b8b5d93 100644 --- a/graphql/e2e/common/schema.go +++ b/graphql/e2e/common/schema.go @@ -74,6 +74,14 @@ const ( { "name": "posts", "description": "" + }, + { + "name": "bio", + "description": "" + }, + { + "name": "rank", + "description": "" } ], "enumValues":[] diff --git a/graphql/e2e/custom_logic/cmd/main.go b/graphql/e2e/custom_logic/cmd/main.go index 5d2c8254661..939b0bd0bf9 100644 --- a/graphql/e2e/custom_logic/cmd/main.go +++ b/graphql/e2e/custom_logic/cmd/main.go @@ -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 { diff --git a/graphql/e2e/custom_logic/custom_logic_test.go b/graphql/e2e/custom_logic/custom_logic_test.go index b70c44f9a2d..ac91ad67af8 100644 --- a/graphql/e2e/custom_logic/custom_logic_test.go +++ b/graphql/e2e/custom_logic/custom_logic_test.go @@ -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)) } @@ -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)) } diff --git a/graphql/e2e/directives/dgraph_directives_test.go b/graphql/e2e/directives/dgraph_directives_test.go index 82a15bd5eec..3eeaca3174b 100644 --- a/graphql/e2e/directives/dgraph_directives_test.go +++ b/graphql/e2e/directives/dgraph_directives_test.go @@ -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" @@ -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()) diff --git a/graphql/e2e/directives/docker-compose.yml b/graphql/e2e/directives/docker-compose.yml index 154fc96fb3a..2cc6fee3efd 100644 --- a/graphql/e2e/directives/docker-compose.yml +++ b/graphql/e2e/directives/docker-compose.yml @@ -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 @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index f6406d00f87..1f750edeef3 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -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") { @@ -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 { @@ -177,4 +180,12 @@ type Person1 { id: ID! name: String! friends: [Person1] @hasInverse(field: friends) -} \ No newline at end of file +} + +type Query { + authorsByName(name: String!): [Author] @lambda +} + +type Mutation { + newAuthor(name: String!): ID! @lambda +} diff --git a/graphql/e2e/directives/script.js b/graphql/e2e/directives/script.js new file mode 100644 index 00000000000..324c009264e --- /dev/null +++ b/graphql/e2e/directives/script.js @@ -0,0 +1,51 @@ +const authorBio = ({parent: {name, dob}}) => `My name is ${name} and I was born on ${dob}.` +const characterBio = ({parent: {name}}) => `My name is ${name}.` +const humanBio = ({parent: {name, totalCredits}}) => `My name is ${name}. I have ${totalCredits} credits.` +const droidBio = ({parent: {name, primaryFunction}}) => `My name is ${name}. My primary function is ${primaryFunction}.` + +async function authorsByName({args, dql}) { + const results = await dql.query(`query queryAuthor($name: string) { + queryAuthor(func: type(test.dgraph.author)) @filter(eq(test.dgraph.author.name, $name)) { + name: test.dgraph.author.name + dob: test.dgraph.author.dob + reputation: test.dgraph.author.reputation + } + }`, {"$name": args.name}) + return results.data.queryAuthor +} + +async function newAuthor({args, graphql}) { + // lets give every new author a reputation of 3 by default + const results = await graphql(`mutation ($name: String!) { + addAuthor(input: [{name: $name, reputation: 3.0 }]) { + author { + id + reputation + } + } + }`, {"name": args.name}) + return results.data.addAuthor.author[0].id +} + +self.addGraphQLResolvers({ + "Author.bio": authorBio, + "Character.bio": characterBio, + "Human.bio": humanBio, + "Droid.bio": droidBio, + "Query.authorsByName": authorsByName, + "Mutation.newAuthor": newAuthor +}) + +async function rank({parents}) { + const idRepList = parents.map(function (parent) { + return {id: parent.id, rep: parent.reputation} + }); + const idRepMap = {}; + idRepList.sort((a, b) => a.rep > b.rep ? -1 : 1) + .forEach((a, i) => idRepMap[a.id] = i + 1) + return parents.map(p => idRepMap[p.id]) +} + +self.addMultiParentGraphQLResolvers({ + "Author.rank": rank +}) \ No newline at end of file diff --git a/graphql/e2e/normal/docker-compose.yml b/graphql/e2e/normal/docker-compose.yml index 154fc96fb3a..2cc6fee3efd 100644 --- a/graphql/e2e/normal/docker-compose.yml +++ b/graphql/e2e/normal/docker-compose.yml @@ -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 @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/graphql/e2e/normal/normal_test.go b/graphql/e2e/normal/normal_test.go index a4ac2f0948d..0dbe13d79c0 100644 --- a/graphql/e2e/normal/normal_test.go +++ b/graphql/e2e/normal/normal_test.go @@ -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" @@ -53,6 +55,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()) diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index fe2675a3244..7e5fb18270f 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -37,6 +37,8 @@ type Author { reputation: Float @search country: Country posts: [Post!] @hasInverse(field: author) + bio: String @lambda + rank: Int @lambda } type Post { @@ -89,6 +91,7 @@ interface Character { id: ID! name: String! @search(by: [exact]) appearsIn: [Episode!] @search + bio: String @lambda } type Human implements Character & Employee { @@ -177,4 +180,12 @@ type Person1 { id: ID! name: String! friends: [Person1] @hasInverse(field: friends) -} \ No newline at end of file +} + +type Query { + authorsByName(name: String!): [Author] @lambda +} + +type Mutation { + newAuthor(name: String!): ID! @lambda +} diff --git a/graphql/e2e/normal/script.js b/graphql/e2e/normal/script.js new file mode 100644 index 00000000000..dcfd429ec34 --- /dev/null +++ b/graphql/e2e/normal/script.js @@ -0,0 +1,51 @@ +const authorBio = ({parent: {name, dob}}) => `My name is ${name} and I was born on ${dob}.` +const characterBio = ({parent: {name}}) => `My name is ${name}.` +const humanBio = ({parent: {name, totalCredits}}) => `My name is ${name}. I have ${totalCredits} credits.` +const droidBio = ({parent: {name, primaryFunction}}) => `My name is ${name}. My primary function is ${primaryFunction}.` + +async function authorsByName({args, dql}) { + const results = await dql.query(`query queryAuthor($name: string) { + queryAuthor(func: type(Author)) @filter(eq(Author.name, $name)) { + name: Author.name + dob: Author.dob + reputation: Author.reputation + } + }`, {"$name": args.name}) + return results.data.queryAuthor +} + +async function newAuthor({args, graphql}) { + // lets give every new author a reputation of 3 by default + const results = await graphql(`mutation ($name: String!) { + addAuthor(input: [{name: $name, reputation: 3.0 }]) { + author { + id + reputation + } + } + }`, {"name": args.name}) + return results.data.addAuthor.author[0].id +} + +self.addGraphQLResolvers({ + "Author.bio": authorBio, + "Character.bio": characterBio, + "Human.bio": humanBio, + "Droid.bio": droidBio, + "Query.authorsByName": authorsByName, + "Mutation.newAuthor": newAuthor +}) + +async function rank({parents}) { + const idRepList = parents.map(function (parent) { + return {id: parent.id, rep: parent.reputation} + }); + const idRepMap = {}; + idRepList.sort((a, b) => a.rep > b.rep ? -1 : 1) + .forEach((a, i) => idRepMap[a.id] = i + 1) + return parents.map(p => idRepMap[p.id]) +} + +self.addMultiParentGraphQLResolvers({ + "Author.rank": rank +}) \ No newline at end of file diff --git a/graphql/e2e/schema/generatedSchema.graphql b/graphql/e2e/schema/generatedSchema.graphql index 38d895ca8ab..0b624a3acd9 100644 --- a/graphql/e2e/schema/generatedSchema.graphql +++ b/graphql/e2e/schema/generatedSchema.graphql @@ -79,6 +79,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/resolve/custom_mutation_test.yaml b/graphql/resolve/custom_mutation_test.yaml index ea6d8a05750..0a6523651cb 100644 --- a/graphql/resolve/custom_mutation_test.yaml +++ b/graphql/resolve/custom_mutation_test.yaml @@ -43,7 +43,7 @@ { "name": "Mov2" } ] } - headers: { "X-App-Token": ["val"], "Auth0-Token": ["tok"] } + headers: { "X-App-Token": ["val"], "Auth0-Token": ["tok"], "Content-type": ["application/json"] } resolvedresponse: | { "createMyFavouriteMovies": [ @@ -104,6 +104,7 @@ "director": [ { "name": "Dir1" } ] } } + headers: { "Content-type": ["application/json"] } resolvedresponse: | { "updateMyFavouriteMovie": { @@ -147,6 +148,7 @@ } url: http://myapi.com/favMovies/0x01 method: DELETE + headers: { "Content-type": ["application/json"] } resolvedresponse: | { "deleteMyFavouriteMovie": { diff --git a/graphql/resolve/custom_query_test.yaml b/graphql/resolve/custom_query_test.yaml index a8875500418..d83f68135c9 100644 --- a/graphql/resolve/custom_query_test.yaml +++ b/graphql/resolve/custom_query_test.yaml @@ -30,6 +30,7 @@ ] url: http://myapi.com/favMovies/0x1?name=Michael&num= method: GET + headers: { "Content-type": ["application/json"] } resolvedresponse: | { "myFavoriteMovies": [ @@ -84,7 +85,7 @@ url: http://myapi.com/favMovies/0x9?name=Michael&num=10 method: POST body: '{ "id": "0x9", "name": "Michael", "director": { "number": 10 }}' - headers: { "X-App-Token": ["val"], "Auth0-Token": ["tok"] } + headers: { "X-App-Token": ["val"], "Auth0-Token": ["tok"], "Content-type": ["application/json"] } resolvedresponse: | { "myFavoriteMoviesPart2": [ diff --git a/graphql/resolve/resolver.go b/graphql/resolve/resolver.go index 7fdbcb69aea..091337792b7 100644 --- a/graphql/resolve/resolver.go +++ b/graphql/resolve/resolver.go @@ -29,6 +29,8 @@ import ( "sync" "time" + "github.com/dgraph-io/dgraph/graphql/authorization" + "github.com/dgraph-io/dgraph/edgraph" "github.com/dgraph-io/dgraph/graphql/dgraph" "github.com/dgraph-io/dgraph/types" @@ -749,7 +751,7 @@ func completeDgraphResult( // it should be using f.DgraphAlias() to get values from valToComplete. // It works ATM because there hasn't been a scenario where there are two fields with same // name in implementing types of an interface with @custom on some field in those types. - err = resolveCustomFields(field.SelectionSet(), valToComplete[field.DgraphAlias()]) + err = resolveCustomFields(ctx, field.SelectionSet(), valToComplete[field.DgraphAlias()]) if err != nil { errs = append(errs, schema.AsGQLErrors(err)...) } @@ -810,7 +812,8 @@ type graphqlResp struct { Errors x.GqlErrorList `json:"errors,omitempty"` } -func resolveCustomField(f schema.Field, vals []interface{}, mu *sync.RWMutex, errCh chan error) { +func resolveCustomField(ctx context.Context, f schema.Field, vals []interface{}, mu *sync.RWMutex, + errCh chan error) { defer api.PanicHandler(func(err error) { errCh <- internalServerError(err, f) }) @@ -876,6 +879,8 @@ func resolveCustomField(f schema.Field, vals []interface{}, mu *sync.RWMutex, er body["query"] = fconf.RemoteGqlQuery body["variables"] = map[string]interface{}{fconf.GraphqlBatchModeArgument: requestInput} requestInput = body + } else if f.HasLambdaDirective() { + requestInput = getBodyForLambda(ctx, f, requestInput, nil) } b, err := json.Marshal(requestInput) @@ -1040,7 +1045,7 @@ func resolveCustomField(f schema.Field, vals []interface{}, mu *sync.RWMutex, er // } // In the example above, resolveNestedFields would be called on classes field and vals would be the // list of all users. -func resolveNestedFields(f schema.Field, vals []interface{}, mu *sync.RWMutex, +func resolveNestedFields(ctx context.Context, f schema.Field, vals []interface{}, mu *sync.RWMutex, errCh chan error) { defer api.PanicHandler(func(err error) { errCh <- internalServerError(err, f) @@ -1119,7 +1124,7 @@ func resolveNestedFields(f schema.Field, vals []interface{}, mu *sync.RWMutex, } mu.RUnlock() - if err := resolveCustomFields(f.SelectionSet(), input); err != nil { + if err := resolveCustomFields(ctx, f.SelectionSet(), input); err != nil { errCh <- err return } @@ -1183,7 +1188,7 @@ func resolveNestedFields(f schema.Field, vals []interface{}, mu *sync.RWMutex, // work. // TODO - We can be smarter about this and know before processing the query if we should be making // this recursive call upfront. -func resolveCustomFields(fields []schema.Field, data interface{}) error { +func resolveCustomFields(ctx context.Context, fields []schema.Field, data interface{}) error { if data == nil { return nil } @@ -1214,9 +1219,9 @@ func resolveCustomFields(fields []schema.Field, data interface{}) error { numRoutines++ hasCustomDirective, _ := f.HasCustomDirective() if !hasCustomDirective { - go resolveNestedFields(f, vals, mu, errCh) + go resolveNestedFields(ctx, f, vals, mu, errCh) } else { - go resolveCustomField(f, vals, mu, errCh) + go resolveCustomField(ctx, f, vals, mu, errCh) } } @@ -1789,6 +1794,23 @@ func makeRequest(client *http.Client, method, url, body string, return b, err } +func getBodyForLambda(ctx context.Context, field schema.Field, parents, + args interface{}) map[string]interface{} { + body := make(map[string]interface{}) + body["resolver"] = field.GetObjectName() + "." + field.Name() + body["authHeader"] = map[string]interface{}{ + "key": authorization.GetHeader(), + "value": authorization.GetJwtToken(ctx), + } + if parents != nil { + body["parents"] = parents + } + if args != nil { + body["args"] = args + } + return body +} + func (hr *httpResolver) rewriteAndExecute(ctx context.Context, field schema.Field) *Resolved { emptyResult := func(err error) *Resolved { return &Resolved{ @@ -1805,7 +1827,11 @@ func (hr *httpResolver) rewriteAndExecute(ctx context.Context, field schema.Fiel var body string if hrc.Template != nil { - b, err := json.Marshal(*hrc.Template) + jsonTemplate := *hrc.Template + if field.HasLambdaDirective() { + jsonTemplate = getBodyForLambda(ctx, field, nil, *hrc.Template) + } + b, err := json.Marshal(jsonTemplate) if err != nil { return emptyResult(jsonMarshalError(err, field, *hrc.Template)) } diff --git a/graphql/schema/dgraph_schemagen_test.yml b/graphql/schema/dgraph_schemagen_test.yml index 0f4ff113f1c..273524c90b2 100644 --- a/graphql/schema/dgraph_schemagen_test.yml +++ b/graphql/schema/dgraph_schemagen_test.yml @@ -594,4 +594,21 @@ schemas: url: "http://mock:8888/users", method: "POST" }) - } \ No newline at end of file + } + - + name: "custom field shouldn't be part of dgraph schema" + input: | + type User { + id: ID! + name: String! + bio: String! @lambda + friends: [User] @custom(http: { + url: "http://mock:8888/users", + method: "GET" + }) + } + output: | + type User { + User.name + } + User.name: string . diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index c001101f544..5da1f1d52cc 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -44,12 +44,18 @@ const ( remoteDirective = "remote" // types with this directive are not stored in Dgraph. cascadeDirective = "cascade" SubscriptionDirective = "withSubscription" + lambdaDirective = "lambda" // custom directive args and fields - dqlArg = "dql" - mode = "mode" - BATCH = "BATCH" - SINGLE = "SINGLE" + dqlArg = "dql" + httpArg = "http" + httpUrl = "url" + httpMethod = "method" + httpBody = "body" + httpGraphql = "graphql" + mode = "mode" + BATCH = "BATCH" + SINGLE = "SINGLE" deprecatedDirective = "deprecated" NumUid = "numUids" @@ -129,6 +135,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int @@ -318,7 +325,8 @@ var directiveValidators = map[string]directiveValidator{ deprecatedDirective: ValidatorNoOp, SubscriptionDirective: ValidatorNoOp, // Just go get it printed into generated schema - authDirective: ValidatorNoOp, + authDirective: ValidatorNoOp, + lambdaDirective: lambdaDirectiveValidation, } var schemaDocValidations []func(schema *ast.SchemaDocument) gqlerror.List @@ -740,10 +748,9 @@ func addPatchType(schema *ast.Schema, defn *ast.Definition) { // } func addFieldFilters(schema *ast.Schema, defn *ast.Definition) { for _, fld := range defn.Fields { - custom := fld.Directives.ForName(customDirective) - // Filtering and ordering for fields with @custom directive is handled by the remote + // Filtering and ordering for fields with @custom/@lambda directive is handled by the remote // endpoint. - if custom != nil { + if hasCustomOrLambda(fld) { continue } @@ -915,12 +922,13 @@ func hasFilterable(defn *ast.Definition) bool { } func hasOrderables(defn *ast.Definition) bool { - return fieldAny(defn.Fields, - func(fld *ast.FieldDefinition) bool { - // lists can't be ordered and NamedType will be empty for lists, - // so it will return false for list fields - return orderable[fld.Type.NamedType] - }) + return fieldAny(defn.Fields, isOrderable) +} + +func isOrderable(fld *ast.FieldDefinition) bool { + // lists can't be ordered and NamedType will be empty for lists, + // so it will return false for list fields + return orderable[fld.Type.NamedType] && !hasCustomOrLambda(fld) } func hasID(defn *ast.Definition) bool { @@ -1041,7 +1049,7 @@ func addTypeOrderable(schema *ast.Schema, defn *ast.Definition) { } for _, fld := range defn.Fields { - if fld.Type.NamedType != "" && orderable[fld.Type.NamedType] { + if isOrderable(fld) { order.EnumValues = append(order.EnumValues, &ast.EnumValueDefinition{Name: fld.Name}) } @@ -1352,9 +1360,9 @@ func getNonIDFields(schema *ast.Schema, defn *ast.Definition) ast.FieldList { continue } - custom := fld.Directives.ForName(customDirective) - // Fields with @custom directive should not be part of mutation input, hence we skip them. - if custom != nil { + // Fields with @custom/@lambda directive should not be part of mutation input, + // hence we skip them. + if hasCustomOrLambda(fld) { continue } @@ -1393,9 +1401,9 @@ func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition) ast.FieldL continue } - custom := fld.Directives.ForName(customDirective) - // Fields with @custom directive should not be part of mutation input, hence we skip them. - if custom != nil { + // Fields with @custom/@lambda directive should not be part of mutation input, + // hence we skip them. + if hasCustomOrLambda(fld) { continue } diff --git a/graphql/schema/gqlschema_test.yml b/graphql/schema/gqlschema_test.yml index f7d08401c18..5f841185677 100644 --- a/graphql/schema/gqlschema_test.yml +++ b/graphql/schema/gqlschema_test.yml @@ -9,7 +9,7 @@ invalid_schemas: } errlist: [ {"message":"Fields id1, id2 and id3 are listed as IDs for type P, but a type can have only one ID field. Pick a single field as the ID for type P.", "locations":[{"line":2, "column":3}, {"line":3, "column":3}, {"line":4, "column":3}]}, - {"message":"Type P; is invalid, a type must have atleast one field that is not of ID! type and doesn't have @custom directive.", "locations":[{"line":1, "column":6}]} + {"message":"Type P; is invalid, a type must have atleast one field that is not of ID! type and doesn't have @custom/@lambda directive.", "locations":[{"line":1, "column":6}]} ] - @@ -33,11 +33,11 @@ invalid_schemas: } errlist: [ {"message":"GraphQL Query and Mutation types are only allowed to have fields - with @custom directive. Other fields are built automatically for you. Found Query getAuthor - without @custom.", "locations":[{"line":1, "column":6}]}, + with @custom/@lambda directive. Other fields are built automatically for you. Found Query getAuthor + without @custom/@lambda.", "locations":[{"line":1, "column":6}]}, {"message":"GraphQL Query and Mutation types are only allowed to have fields with - @custom directive. Other fields are built automatically for you. Found Mutation getAuthor - without @custom.", "locations":[{"line":4, "column":6}]}, + @custom/@lambda directive. Other fields are built automatically for you. Found Mutation getAuthor + without @custom/@lambda.", "locations":[{"line":4, "column":6}]}, ] - @@ -1574,7 +1574,7 @@ invalid_schemas: ] }, { - "message": "Type Author; Field name; @custom directive, body template can't use another field with @custom directive, found field `name` with @custom.", + "message": "Type Author; Field name; @custom directive, body template can't use another field with @custom/@lambda directive, found field `name` with @custom/@lambda.", "locations": [ { "line": 6, @@ -1656,7 +1656,7 @@ invalid_schemas: }) } errlist: [ - {"message": "Type Author; Field yo; @custom directive, body template can't use another field with @custom directive, found field `foo` with @custom.", + {"message": "Type Author; Field yo; @custom directive, body template can't use another field with @custom/@lambda directive, found field `foo` with @custom/@lambda.", "locations":[{"line":12, "column":12}]}, ] @@ -1701,7 +1701,7 @@ invalid_schemas: ] }, { - "message": "Type Author; Field name; @custom directive, graphql can't use another field with @custom directive, found field `name` with @custom.", + "message": "Type Author; Field name; @custom directive, graphql can't use another field with @custom/@lambda directive, found field `name` with @custom/@lambda.", "locations": [ { "line": 6, @@ -1807,7 +1807,7 @@ invalid_schemas: } errlist: [ { - "message": "Type Author; Field yo; @custom directive, graphql can't use another field with @custom directive, found field `foo` with @custom.", + "message": "Type Author; Field yo; @custom directive, graphql can't use another field with @custom/@lambda directive, found field `foo` with @custom/@lambda.", "locations": [ { "line": 12, @@ -2297,7 +2297,7 @@ invalid_schemas: } errlist: [ - {"message": "Type Author; is invalid, a type must have atleast one field that is not of ID! type and doesn't have @custom directive.", + {"message": "Type Author; is invalid, a type must have atleast one field that is not of ID! type and doesn't have @custom/@lambda directive.", "locations":[{"line":1, "column":6}]}, ] @@ -2314,7 +2314,7 @@ invalid_schemas: }) } errlist: [ - {"message": "Type Author; is invalid, a type must have atleast one field that is not of ID! type and doesn't have @custom directive.", + {"message": "Type Author; is invalid, a type must have atleast one field that is not of ID! type and doesn't have @custom/@lambda directive.", "locations":[{"line":1, "column":6}]}, ] @@ -2368,8 +2368,8 @@ invalid_schemas: }) } errlist: [ - {"message": "Type School; field name; can't have @custom directive as a @remote - type can't have fields with @custom directive.", "locations": [{"line":9, "column":3}]} + {"message": "Type School; field name; can't have @custom/@lambda directive as a @remote + type can't have fields with @custom/@lambda directive.", "locations": [{"line":9, "column":3}]} ] - @@ -2389,7 +2389,7 @@ invalid_schemas: } errlist: [ {"message": "Type Author; field neighbour; is of a type that has @remote directive. Those - would need to be resolved by a @custom directive.", + would need to be resolved by a @custom/@lambda directive.", "locations": [{"line":9, "column":3}]} ] diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index 58f68c69435..524be72d407 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -425,13 +425,12 @@ func nameCheck(schema *ast.Schema, defn *ast.Definition) gqlerror.List { if isQueryOrMutationType(defn) { for _, fld := range defn.Fields { - // If we find any query or mutation field defined without a @custom directive, that - // is an error for us. - custom := fld.Directives.ForName(customDirective) - if custom == nil { + // If we find any query or mutation field defined without a @custom/@lambda + // directive, that is an error for us. + if !hasCustomOrLambda(fld) { errMesg = "GraphQL Query and Mutation types are only allowed to have fields " + - "with @custom directive. Other fields are built automatically for you. " + - "Found " + defn.Name + " " + fld.Name + " without @custom." + "with @custom/@lambda directive. Other fields are built automatically for" + + " you. Found " + defn.Name + " " + fld.Name + " without @custom/@lambda." break } } @@ -581,8 +580,7 @@ func nonIdFieldsCheck(schema *ast.Schema, typ *ast.Definition) gqlerror.List { hasNonIdField := false for _, field := range typ.Fields { - custom := field.Directives.ForName(customDirective) - if isIDField(typ, field) || custom != nil { + if isIDField(typ, field) || hasCustomOrLambda(field) { continue } hasNonIdField = true @@ -591,7 +589,8 @@ func nonIdFieldsCheck(schema *ast.Schema, typ *ast.Definition) gqlerror.List { if !hasNonIdField { return []*gqlerror.Error{gqlerror.ErrorPosf(typ.Position, "Type %s; is invalid, a type must have atleast "+ - "one field that is not of ID! type and doesn't have @custom directive.", typ.Name)} + "one field that is not of ID! type and doesn't have @custom/@lambda directive.", + typ.Name)} } return nil } @@ -605,8 +604,7 @@ func remoteTypeValidation(schema *ast.Schema, typ *ast.Definition) gqlerror.List for _, field := range typ.Fields { // If the field is being resolved through a custom directive, then we don't care if // the type for the field is a remote or a non-remote type. - custom := field.Directives.ForName(customDirective) - if custom != nil { + if hasCustomOrLambda(field) { continue } t := field.Type.Name() @@ -615,7 +613,7 @@ func remoteTypeValidation(schema *ast.Schema, typ *ast.Definition) gqlerror.List if remoteDir != nil { return []*gqlerror.Error{gqlerror.ErrorPosf(field.Position, "Type %s; "+ "field %s; is of a type that has @remote directive. Those would need to be "+ - "resolved by a @custom directive.", typ.Name, field.Name)} + "resolved by a @custom/@lambda directive.", typ.Name, field.Name)} } } @@ -633,11 +631,10 @@ func remoteTypeValidation(schema *ast.Schema, typ *ast.Definition) gqlerror.List // This means that the type was a remote type. for _, field := range typ.Fields { - custom := field.Directives.ForName(customDirective) - if custom != nil { + if hasCustomOrLambda(field) { return []*gqlerror.Error{gqlerror.ErrorPosf(field.Position, "Type %s; "+ - "field %s; can't have @custom directive as a @remote type can't have fields with"+ - " @custom directive.", typ.Name, field.Name)} + "field %s; can't have @custom/@lambda directive as a @remote type can't have"+ + " fields with @custom/@lambda directive.", typ.Name, field.Name)} } } @@ -1181,6 +1178,28 @@ func passwordValidation(sch *ast.Schema, return passwordDirectiveValidation(sch, typ) } +func lambdaDirectiveValidation(sch *ast.Schema, + typ *ast.Definition, + field *ast.FieldDefinition, + dir *ast.Directive, + secrets map[string]x.SensitiveByteSlice) gqlerror.List { + // if the lambda url wasn't specified during alpha startup, + // just return that error. Don't confuse the user with errors from @custom yet. + if x.Config.GraphqlLambdaUrl == "" { + return []*gqlerror.Error{gqlerror.ErrorPosf(dir.Position, + "Type %s; Field %s: has the @lambda directive, but the "+ + "`--graphql_lambda_url` flag wasn't specified during alpha startup.", + typ.Name, field.Name)} + } + // reuse @custom directive validation + errs := customDirectiveValidation(sch, typ, field, buildCustomDirectiveForLambda(typ, field, + dir, func(f *ast.FieldDefinition) bool { return false }), secrets) + for _, err := range errs { + err.Message = "While building @custom for @lambda: " + err.Message + } + return errs +} + func customDirectiveValidation(sch *ast.Schema, typ *ast.Definition, field *ast.FieldDefinition, @@ -1228,7 +1247,7 @@ func customDirectiveValidation(sch *ast.Schema, typ.Name, field.Name, l)) } - httpArg := dir.Arguments.ForName("http") + httpArg := dir.Arguments.ForName(httpArg) dqlArg := dir.Arguments.ForName(dqlArg) if httpArg == nil && dqlArg == nil { @@ -1301,7 +1320,7 @@ func customDirectiveValidation(sch *ast.Schema, // Start validating children of http argument // 4. Validating url - httpUrl := httpArg.Value.Children.ForName("url") + httpUrl := httpArg.Value.Children.ForName(httpUrl) if httpUrl == nil { errs = append(errs, gqlerror.ErrorPosf( dir.Position, @@ -1377,7 +1396,7 @@ func customDirectiveValidation(sch *ast.Schema, } // 5. Validating method - method := httpArg.Value.Children.ForName("method") + method := httpArg.Value.Children.ForName(httpMethod) if method == nil { errs = append(errs, gqlerror.ErrorPosf( dir.Position, @@ -1422,8 +1441,8 @@ func customDirectiveValidation(sch *ast.Schema, } // 7. Validating graphql combination with url params, method and body - body := httpArg.Value.Children.ForName("body") - graphql := httpArg.Value.Children.ForName("graphql") + body := httpArg.Value.Children.ForName(httpBody) + graphql := httpArg.Value.Children.ForName(httpGraphql) if graphql != nil { if urlHasParams { errs = append(errs, gqlerror.ErrorPosf(dir.Position, @@ -1689,11 +1708,11 @@ func customDirectiveValidation(sch *ast.Schema, fname, typName)) } - if fd.Directives.ForName(customDirective) != nil { + if hasCustomOrLambda(fd) { errs = append(errs, gqlerror.ErrorPosf(errPos, "Type %s; Field %s; @custom directive, %s can't use another field with "+ - "@custom directive, found field `%s` with @custom.", typ.Name, - field.Name, errIn, fname)) + "@custom/@lambda directive, found field `%s` with @custom/@lambda.", + typ.Name, field.Name, errIn, fname)) } if fname == idField || fname == xidField { diff --git a/graphql/schema/schemagen.go b/graphql/schema/schemagen.go index dc42c2fb86a..8e011dc32b8 100644 --- a/graphql/schema/schemagen.go +++ b/graphql/schema/schemagen.go @@ -395,7 +395,7 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string) string { pwdField := getPasswordField(def) for _, f := range def.Fields { - if f.Type.Name() == "ID" { + if f.Type.Name() == "ID" || hasCustomOrLambda(f) { continue } diff --git a/graphql/schema/schemagen_test.go b/graphql/schema/schemagen_test.go index f69b0b32dc4..35173540629 100644 --- a/graphql/schema/schemagen_test.go +++ b/graphql/schema/schemagen_test.go @@ -18,6 +18,7 @@ package schema import ( "io/ioutil" + "os" "strings" "testing" @@ -309,3 +310,10 @@ func TestOnlyCorrectSearchArgsWork(t *testing.T) { }) } } + +func TestMain(m *testing.M) { + // set up the lambda url for unit tests + x.Config.GraphqlLambdaUrl = "http://localhost:8086/graphql-worker" + // now run the tests + os.Exit(m.Run()) +} diff --git a/graphql/schema/testdata/schemagen/input/lambda-directive.graphql b/graphql/schema/testdata/schemagen/input/lambda-directive.graphql new file mode 100644 index 00000000000..273f3ee54e9 --- /dev/null +++ b/graphql/schema/testdata/schemagen/input/lambda-directive.graphql @@ -0,0 +1,14 @@ +type User { + id: ID! + firstName: String! + lastName: String! + fullName: String @lambda +} + +type Query { + queryUserNames(id: [ID!]!): [String] @lambda +} + +type Mutation { + createUser(firstName: String!, lastName: String!): User @lambda +} \ No newline at end of file diff --git a/graphql/schema/testdata/schemagen/output/authorization.graphql b/graphql/schema/testdata/schemagen/output/authorization.graphql index cfa0d319bd9..44e4790a031 100644 --- a/graphql/schema/testdata/schemagen/output/authorization.graphql +++ b/graphql/schema/testdata/schemagen/output/authorization.graphql @@ -90,6 +90,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql b/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql index de76461219f..b84a1409edd 100755 --- a/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql +++ b/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql @@ -95,6 +95,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/custom-mutation.graphql b/graphql/schema/testdata/schemagen/output/custom-mutation.graphql index 4cb66a6d07c..ed6e901dbde 100644 --- a/graphql/schema/testdata/schemagen/output/custom-mutation.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-mutation.graphql @@ -83,6 +83,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql b/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql index baca392f26c..c2966a5862f 100755 --- a/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql @@ -100,6 +100,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql b/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql index c62940c1f5b..2a1fc063a87 100644 --- a/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql @@ -84,6 +84,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/custom-query-not-dgraph-type.graphql b/graphql/schema/testdata/schemagen/output/custom-query-not-dgraph-type.graphql index f0c787c2e94..cf1052f02d0 100755 --- a/graphql/schema/testdata/schemagen/output/custom-query-not-dgraph-type.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-query-not-dgraph-type.graphql @@ -83,6 +83,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/custom-query-with-dgraph-type.graphql b/graphql/schema/testdata/schemagen/output/custom-query-with-dgraph-type.graphql index 188ee9a56b6..94ba91e7d72 100755 --- a/graphql/schema/testdata/schemagen/output/custom-query-with-dgraph-type.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-query-with-dgraph-type.graphql @@ -79,6 +79,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/deprecated.graphql b/graphql/schema/testdata/schemagen/output/deprecated.graphql index d05029e2691..6b041d23f8c 100755 --- a/graphql/schema/testdata/schemagen/output/deprecated.graphql +++ b/graphql/schema/testdata/schemagen/output/deprecated.graphql @@ -79,6 +79,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-on-concrete-type-with-interfaces.graphql b/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-on-concrete-type-with-interfaces.graphql index cc51bc4e836..3d3086362b4 100755 --- a/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-on-concrete-type-with-interfaces.graphql +++ b/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-on-concrete-type-with-interfaces.graphql @@ -93,6 +93,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-with-interfaces.graphql b/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-with-interfaces.graphql index 88d0edf56bf..9469d7b6161 100755 --- a/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-with-interfaces.graphql +++ b/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-with-interfaces.graphql @@ -93,6 +93,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql b/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql index bf7e45e6acd..0412f042de9 100755 --- a/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql @@ -92,6 +92,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/field-with-reverse-predicate-in-dgraph-directive.graphql b/graphql/schema/testdata/schemagen/output/field-with-reverse-predicate-in-dgraph-directive.graphql index 3def6cff8e7..130f5afc9ad 100755 --- a/graphql/schema/testdata/schemagen/output/field-with-reverse-predicate-in-dgraph-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/field-with-reverse-predicate-in-dgraph-directive.graphql @@ -86,6 +86,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql index 7850a1369cb..38eb882a021 100644 --- a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql @@ -89,6 +89,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface-having-directive.graphql b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface-having-directive.graphql index 50f0faca1a7..47ab5c5dc34 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface-having-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface-having-directive.graphql @@ -103,6 +103,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql index 7b09a60441c..0d297312fb5 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql @@ -104,6 +104,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/hasInverse-with-type-having-directive.graphql b/graphql/schema/testdata/schemagen/output/hasInverse-with-type-having-directive.graphql index 50f0faca1a7..47ab5c5dc34 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse-with-type-having-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse-with-type-having-directive.graphql @@ -103,6 +103,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/hasInverse.graphql b/graphql/schema/testdata/schemagen/output/hasInverse.graphql index 4f99a739079..fade3710070 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse.graphql @@ -84,6 +84,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql b/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql index fd4661da0e7..3bfc317ea9a 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql @@ -84,6 +84,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql b/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql index 16a4b071f94..3ff46e78d1e 100755 --- a/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql @@ -86,6 +86,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql b/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql index 80e566e6462..854d28418d6 100644 --- a/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql @@ -93,6 +93,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql b/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql index 46da5584d0a..b95d614cbf6 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql @@ -88,6 +88,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql b/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql index 44793b34847..04c8b76b0cc 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql @@ -88,6 +88,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/interfaces-with-types-and-password.graphql b/graphql/schema/testdata/schemagen/output/interfaces-with-types-and-password.graphql index 2df8fdc2418..f1728af4d90 100755 --- a/graphql/schema/testdata/schemagen/output/interfaces-with-types-and-password.graphql +++ b/graphql/schema/testdata/schemagen/output/interfaces-with-types-and-password.graphql @@ -110,6 +110,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql b/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql index 9a28357195a..38539ebd863 100755 --- a/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql +++ b/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql @@ -110,6 +110,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/lambda-directive.graphql b/graphql/schema/testdata/schemagen/output/lambda-directive.graphql new file mode 100644 index 00000000000..2232f907823 --- /dev/null +++ b/graphql/schema/testdata/schemagen/output/lambda-directive.graphql @@ -0,0 +1,245 @@ +####################### +# Input Schema +####################### + +type User { + id: ID! + firstName: String! + lastName: String! + fullName: String @lambda +} + +####################### +# Extended Definitions +####################### + +""" +The Int64 scalar type represents a signed 64‐bit numeric non‐fractional value. +Int64 can represent values in range [-(2^63),(2^63 - 1)]. +""" +scalar Int64 + +""" +The DateTime scalar type represents date and time as a string in RFC3339 format. +For example: "1985-04-12T23:20:50.52Z" represents 20 minutes and 50.52 seconds after the 23rd hour of April 12th, 1985 in UTC. +""" +scalar DateTime + +enum DgraphIndex { + int + int64 + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour +} + +input AuthRule { + and: [AuthRule] + or: [AuthRule] + not: AuthRule + rule: String +} + +enum HTTPMethod { + GET + POST + PUT + PATCH + DELETE +} + +enum Mode { + BATCH + SINGLE +} + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +directive @hasInverse(field: String!) on FIELD_DEFINITION +directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION +directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION +directive @id on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete:AuthRule) on OBJECT +directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE +directive @cascade(fields: [String]) on FIELD +directive @lambda on FIELD_DEFINITION + +input IntFilter { + eq: Int + le: Int + lt: Int + ge: Int + gt: Int +} + +input Int64Filter { + eq: Int64 + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 +} + +input FloatFilter { + eq: Float + le: Float + lt: Float + ge: Float + gt: Float +} + +input DateTimeFilter { + eq: DateTime + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime +} + +input StringTermFilter { + allofterms: String + anyofterms: String +} + +input StringRegExpFilter { + regexp: String +} + +input StringFullTextFilter { + alloftext: String + anyoftext: String +} + +input StringExactFilter { + eq: String + le: String + lt: String + ge: String + gt: String +} + +input StringHashFilter { + eq: String +} + +####################### +# Generated Types +####################### + +type AddUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + numUids: Int +} + +type DeleteUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + msg: String + numUids: Int +} + +type UpdateUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + numUids: Int +} + +####################### +# Generated Enums +####################### + +enum UserHasFilter { + firstName + lastName +} + +enum UserOrderable { + firstName + lastName +} + +####################### +# Generated Inputs +####################### + +input AddUserInput { + firstName: String! + lastName: String! +} + +input UpdateUserInput { + filter: UserFilter! + set: UserPatch + remove: UserPatch +} + +input UserFilter { + id: [ID!] + has: UserHasFilter + and: UserFilter + or: UserFilter + not: UserFilter +} + +input UserOrder { + asc: UserOrderable + desc: UserOrderable + then: UserOrder +} + +input UserPatch { + firstName: String + lastName: String +} + +input UserRef { + id: ID + firstName: String + lastName: String +} + +####################### +# Generated Query +####################### + +type Query { + queryUserNames(id: [ID!]!): [String] @lambda + getUser(id: ID!): User + queryUser(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] +} + +####################### +# Generated Mutations +####################### + +type Mutation { + createUser(firstName: String!, lastName: String!): User @lambda + addUser(input: [AddUserInput!]!): AddUserPayload + updateUser(input: UpdateUserInput!): UpdateUserPayload + deleteUser(filter: UserFilter!): DeleteUserPayload +} + diff --git a/graphql/schema/testdata/schemagen/output/no-id-field-with-searchables.graphql b/graphql/schema/testdata/schemagen/output/no-id-field-with-searchables.graphql index b8c18e78a43..3fe7133ba86 100755 --- a/graphql/schema/testdata/schemagen/output/no-id-field-with-searchables.graphql +++ b/graphql/schema/testdata/schemagen/output/no-id-field-with-searchables.graphql @@ -78,6 +78,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/no-id-field.graphql b/graphql/schema/testdata/schemagen/output/no-id-field.graphql index 17e2fe9c24b..168d53f915c 100755 --- a/graphql/schema/testdata/schemagen/output/no-id-field.graphql +++ b/graphql/schema/testdata/schemagen/output/no-id-field.graphql @@ -90,6 +90,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/password-type.graphql b/graphql/schema/testdata/schemagen/output/password-type.graphql index 162596735d2..03e864101fe 100755 --- a/graphql/schema/testdata/schemagen/output/password-type.graphql +++ b/graphql/schema/testdata/schemagen/output/password-type.graphql @@ -79,6 +79,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/searchables-references.graphql b/graphql/schema/testdata/schemagen/output/searchables-references.graphql index a16f2f20a5b..29e8578d236 100755 --- a/graphql/schema/testdata/schemagen/output/searchables-references.graphql +++ b/graphql/schema/testdata/schemagen/output/searchables-references.graphql @@ -88,6 +88,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/searchables.graphql b/graphql/schema/testdata/schemagen/output/searchables.graphql index 92eccb2f04f..566261f21f1 100755 --- a/graphql/schema/testdata/schemagen/output/searchables.graphql +++ b/graphql/schema/testdata/schemagen/output/searchables.graphql @@ -105,6 +105,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql b/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql index 907d07108b7..82a8fc8fb93 100755 --- a/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql +++ b/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql @@ -87,6 +87,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/single-type.graphql b/graphql/schema/testdata/schemagen/output/single-type.graphql index e62e29f57da..1306205fa06 100755 --- a/graphql/schema/testdata/schemagen/output/single-type.graphql +++ b/graphql/schema/testdata/schemagen/output/single-type.graphql @@ -81,6 +81,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql b/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql index 9779f10fa44..351a9970e93 100755 --- a/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql +++ b/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql @@ -94,6 +94,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/type-reference.graphql b/graphql/schema/testdata/schemagen/output/type-reference.graphql index 85272795aa2..0e94864fd7f 100755 --- a/graphql/schema/testdata/schemagen/output/type-reference.graphql +++ b/graphql/schema/testdata/schemagen/output/type-reference.graphql @@ -86,6 +86,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/type-with-arguments-on-field.graphql b/graphql/schema/testdata/schemagen/output/type-with-arguments-on-field.graphql index ff2f410fd76..b3390d7be0f 100644 --- a/graphql/schema/testdata/schemagen/output/type-with-arguments-on-field.graphql +++ b/graphql/schema/testdata/schemagen/output/type-with-arguments-on-field.graphql @@ -87,6 +87,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/type-with-custom-field-on-dgraph-type.graphql b/graphql/schema/testdata/schemagen/output/type-with-custom-field-on-dgraph-type.graphql index a221255dac3..4aa5d49a1f5 100644 --- a/graphql/schema/testdata/schemagen/output/type-with-custom-field-on-dgraph-type.graphql +++ b/graphql/schema/testdata/schemagen/output/type-with-custom-field-on-dgraph-type.graphql @@ -86,6 +86,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int @@ -182,7 +183,6 @@ enum CarOrderable { } enum UserOrderable { - name age } diff --git a/graphql/schema/testdata/schemagen/output/type-with-custom-fields-on-remote-type.graphql b/graphql/schema/testdata/schemagen/output/type-with-custom-fields-on-remote-type.graphql index 03953098fad..9871057598c 100644 --- a/graphql/schema/testdata/schemagen/output/type-with-custom-fields-on-remote-type.graphql +++ b/graphql/schema/testdata/schemagen/output/type-with-custom-fields-on-remote-type.graphql @@ -86,6 +86,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int @@ -162,7 +163,6 @@ type UpdateUserPayload { ####################### enum UserOrderable { - name age } diff --git a/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql b/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql index a9d26c25384..5b55fbba2a3 100644 --- a/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql +++ b/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql @@ -81,6 +81,7 @@ directive @auth( directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION directive @remote on OBJECT | INTERFACE directive @cascade on FIELD +directive @lambda on FIELD_DEFINITION input IntFilter { eq: Int diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index 70e5bda1f42..1514d234855 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -127,6 +127,7 @@ type Field interface { Include() bool Cascade() bool HasCustomDirective() (bool, map[string]bool) + HasLambdaDirective() bool Type() Type SelectionSet() []Field Location() x.Location @@ -227,6 +228,9 @@ type schema struct { // something like field.Directives.ForName("custom"), which results in iterating over all the // directives of the field. customDirectives map[string]map[string]*ast.Directive + // lambdaDirectives stores the mapping of typeName->fieldName->true, if the field has @lambda. + // It is read-only. + lambdaDirectives map[string]map[string]bool // Map from typename to auth rules authRules map[string]*TypeAuth } @@ -603,34 +607,167 @@ func repeatedFieldMappings(s *ast.Schema, dgPreds map[string]map[string]string) return repeatedFieldNames } -func customMappings(s *ast.Schema) map[string]map[string]*ast.Directive { +// customAndLambdaMappings does following things: +// * If there is @custom on any field, it removes the directive from the list of directives on +// that field. Instead, it puts it in a map of typeName->fieldName->custom directive definition. +// This mapping is returned as the first return value, which is later used to determine if some +// field has custom directive or not, and accordingly construct the HTTP request for the field. +// * If there is @lambda on any field, it removes the directive from the list of directives on +// that field. Instead, it puts it in a map of typeName->fieldName->bool. This mapping is returned +// as the second return value, which is later used to determine if some field has lambda directive +// or not. An appropriate @custom directive is also constructed for the field with @lambda and +// put into the first mapping. Both of these mappings together are used to construct the HTTP +// request for @lambda field. Internally, @lambda is just @custom(http: { +// url: "", +// method: POST, +// body: "/" +// mode: BATCH (set only if @lambda was on a non query/mutation field) +// }) +// So, by constructing an appropriate custom directive for @lambda fields, +// we just reuse logic from @custom. +func customAndLambdaMappings(s *ast.Schema) (map[string]map[string]*ast.Directive, + map[string]map[string]bool) { customDirectives := make(map[string]map[string]*ast.Directive) + lambdaDirectives := make(map[string]map[string]bool) for _, typ := range s.Types { for _, field := range typ.Fields { for i, dir := range field.Directives { - if dir.Name == customDirective { - // remove custom directive from s + if dir.Name == customDirective || dir.Name == lambdaDirective { + // remove @custom/@lambda directive from s lastIndex := len(field.Directives) - 1 field.Directives[i] = field.Directives[lastIndex] field.Directives = field.Directives[:lastIndex] - // now put it into mapping - var fieldMap map[string]*ast.Directive - if innerMap, ok := customDirectives[typ.Name]; !ok { - fieldMap = make(map[string]*ast.Directive) + // get the @custom mapping for this type + var customFieldMap map[string]*ast.Directive + if existingCustomFieldMap, ok := customDirectives[typ.Name]; ok { + customFieldMap = existingCustomFieldMap + } else { + customFieldMap = make(map[string]*ast.Directive) + } + + if dir.Name == customDirective { + // if it was @custom, put the directive at the @custom mapping for the field + customFieldMap[field.Name] = dir } else { - fieldMap = innerMap + // for lambda, first update the lambda directives map + var lambdaFieldMap map[string]bool + if existingLambdaFieldMap, ok := lambdaDirectives[typ.Name]; ok { + lambdaFieldMap = existingLambdaFieldMap + } else { + lambdaFieldMap = make(map[string]bool) + } + lambdaFieldMap[field.Name] = true + lambdaDirectives[typ.Name] = lambdaFieldMap + // then, build a custom directive with correct semantics to be put + // into custom directives map at this field + customFieldMap[field.Name] = buildCustomDirectiveForLambda(typ, field, + dir, func(f *ast.FieldDefinition) bool { + // Need to skip the fields which have a @custom/@lambda from + // going in body template. The field itself may not have the + // directive anymore because the directive may have been removed by + // this function already. So, using these maps to find the same. + return lambdaFieldMap[f.Name] || customFieldMap[f.Name] != nil + }) } - fieldMap[field.Name] = dir - customDirectives[typ.Name] = fieldMap - // break, as there can only be one @custom + // finally, update the custom directives map for this type + customDirectives[typ.Name] = customFieldMap + // break, as there can only be one @custom/@lambda break } } } } - return customDirectives + return customDirectives, lambdaDirectives +} + +func hasCustomOrLambda(f *ast.FieldDefinition) bool { + for _, dir := range f.Directives { + if dir.Name == customDirective || dir.Name == lambdaDirective { + return true + } + } + return false +} + +// buildCustomDirectiveForLambda returns custom directive for the given field to be used for @lambda +// The constructed @custom looks like this: +// @custom(http: { +// url: "", +// method: POST, +// body: "/" +// mode: BATCH (set only if @lambda was on a non query/mutation field) +// }) +func buildCustomDirectiveForLambda(defn *ast.Definition, field *ast.FieldDefinition, + lambdaDir *ast.Directive, skipInBodyTemplate func(f *ast.FieldDefinition) bool) *ast.Directive { + comma := "" + var bodyTemplate strings.Builder + + // this function appends a variable to the body template for @custom + appendToBodyTemplate := func(varName string) { + bodyTemplate.WriteString(comma) + bodyTemplate.WriteString(varName) + bodyTemplate.WriteString(": $") + bodyTemplate.WriteString(varName) + comma = ", " + } + + // first let's construct the body template for the custom directive + bodyTemplate.WriteString("{") + if isQueryOrMutationType(defn) { + // for queries and mutations we need to put their arguments in the body template + for _, arg := range field.Arguments { + appendToBodyTemplate(arg.Name) + } + } else { + // For fields in other types, skip the ones in body template which have a @lambda or @custom + // or are not scalar. The skipInBodyTemplate function is also used to check these + // conditions, in case the field can't tell by itself. + for _, f := range defn.Fields { + if hasCustomOrLambda(f) || !isScalar(f.Type.Name()) || skipInBodyTemplate(f) { + continue + } + appendToBodyTemplate(f.Name) + } + } + bodyTemplate.WriteString("}") + + // build the children for http argument + httpArgChildrens := []*ast.ChildValue{ + getChildValue(httpUrl, x.Config.GraphqlLambdaUrl, ast.StringValue, lambdaDir.Position), + getChildValue(httpMethod, http.MethodPost, ast.EnumValue, lambdaDir.Position), + getChildValue(httpBody, bodyTemplate.String(), ast.StringValue, lambdaDir.Position), + } + if !isQueryOrMutationType(defn) { + httpArgChildrens = append(httpArgChildrens, + getChildValue(mode, BATCH, ast.EnumValue, lambdaDir.Position)) + } + + // build the custom directive + return &ast.Directive{ + Name: customDirective, + Arguments: []*ast.Argument{{ + Name: httpArg, + Value: &ast.Value{ + Kind: ast.ObjectValue, + Children: httpArgChildrens, + Position: lambdaDir.Position, + }, + Position: lambdaDir.Position, + }}, + Position: lambdaDir.Position, + } +} + +func getChildValue(name, raw string, kind ast.ValueKind, position *ast.Position) *ast.ChildValue { + return &ast.ChildValue{ + Name: name, + Value: &ast.Value{Raw: raw, Kind: kind, Position: position}, + Position: position, + } } // AsSchema wraps a github.com/vektah/gqlparser/ast.Schema. @@ -642,13 +779,15 @@ func AsSchema(s *ast.Schema) (Schema, error) { return nil, err } + customDirs, lambdaDirs := customAndLambdaMappings(s) dgraphPredicate := dgraphMapping(s) sch := &schema{ schema: s, dgraphPredicate: dgraphPredicate, typeNameAst: typeMappings(s), repeatedFieldNames: repeatedFieldMappings(s, dgraphPredicate), - customDirectives: customMappings(s), + customDirectives: customDirs, + lambdaDirectives: lambdaDirs, authRules: authRules, } sch.mutatedType = mutatedTypeMapping(sch, dgraphPredicate) @@ -822,6 +961,10 @@ func (f *field) HasCustomDirective() (bool, map[string]bool) { return true, rf } +func (f *field) HasLambdaDirective() bool { + return f.op.inSchema.lambdaDirectives[f.GetObjectName()][f.Name()] +} + func (f *field) XIDArg() string { xidArgName := "" passwordField := f.Type().PasswordField() @@ -952,6 +1095,8 @@ func getCustomHTTPConfig(f *field, isQueryOrMutation bool) (FieldHTTPConfig, err } fconf.ForwardHeaders = http.Header{} + // set application/json as the default Content-Type + fconf.ForwardHeaders.Set("Content-Type", "application/json") secretHeaders := httpArg.Value.Children.ForName("secretHeaders") if secretHeaders != nil { hc.RLock() @@ -1180,6 +1325,10 @@ func (q *query) HasCustomDirective() (bool, map[string]bool) { return (*field)(q).HasCustomDirective() } +func (q *query) HasLambdaDirective() bool { + return (*field)(q).HasLambdaDirective() +} + func (q *query) IDArgValue() (*string, uint64, error) { return (*field)(q).IDArgValue() } @@ -1313,6 +1462,10 @@ func (m *mutation) HasCustomDirective() (bool, map[string]bool) { return (*field)(m).HasCustomDirective() } +func (m *mutation) HasLambdaDirective() bool { + return (*field)(m).HasLambdaDirective() +} + func (m *mutation) Type() Type { return (*field)(m).Type() } diff --git a/x/config.go b/x/config.go index 5390720710a..8a6e78d660b 100644 --- a/x/config.go +++ b/x/config.go @@ -36,6 +36,8 @@ type Options struct { GraphqlExtension bool // GraphqlDebug will enable debug mode in GraphQL GraphqlDebug bool + // GraphqlLambdaUrl stores the URL of lambda functions for custom GraphQL resolvers + GraphqlLambdaUrl string } // Config stores the global instance of this package's options.