diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index e47bcf5463f..ee08d8667aa 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -579,7 +579,7 @@ func extractUserAndGroups(ctx context.Context) ([]string, error) { if err != nil { return nil, err } - return validateToken(accessJwt[0]) + return validateToken(accessJwt) } type authPredResult struct { diff --git a/graphql/admin/add_group.go b/graphql/admin/add_group.go index dae459fa554..4245e0744e6 100644 --- a/graphql/admin/add_group.go +++ b/graphql/admin/add_group.go @@ -60,6 +60,13 @@ func (mrw *addGroupRewriter) FromMutationResult( return ((*resolve.AddRewriter)(mrw)).FromMutationResult(ctx, mutation, assigned, result) } +func (mrw *addGroupRewriter) MutatedRootUIDs( + mutation schema.Mutation, + assigned map[string]string, + result map[string]interface{}) []string { + return ((*resolve.AddRewriter)(mrw)).MutatedRootUIDs(mutation, assigned, result) +} + // removeDuplicateRuleRef removes duplicate rules based on predicate value. // for duplicate rules, only the last rule with duplicate predicate name is preserved. func removeDuplicateRuleRef(rules []interface{}) ([]interface{}, x.GqlErrorList) { diff --git a/graphql/admin/admin.go b/graphql/admin/admin.go index f15ae32fea7..c8c93e627fc 100644 --- a/graphql/admin/admin.go +++ b/graphql/admin/admin.go @@ -397,7 +397,7 @@ func SchemaValidate(sch string) error { return err } - _, err = schema.FromString(schHandler.GQLSchema()) + _, err = schema.FromString(schHandler.GQLSchema(), x.GalaxyNamespace) return err } @@ -460,7 +460,7 @@ type adminServer struct { // main /graphql endpoint and an admin server. The result is mainServer, adminServer. func NewServers(withIntrospection bool, globalEpoch map[uint64]*uint64, closer *z.Closer) (IServeGraphQL, IServeGraphQL, *GraphQLHealthStore) { - gqlSchema, err := schema.FromString("") + gqlSchema, err := schema.FromString("", x.GalaxyNamespace) if err != nil { x.Panic(err) } @@ -493,7 +493,7 @@ func newAdminResolver( epoch map[uint64]*uint64, closer *z.Closer) *resolve.RequestResolver { - adminSchema, err := schema.FromString(graphqlAdminSchema) + adminSchema, err := schema.FromString(graphqlAdminSchema, x.GalaxyNamespace) if err != nil { x.Panic(err) } @@ -561,7 +561,7 @@ func newAdminResolver( var gqlSchema schema.Schema // on drop_all, we will receive an empty string as the schema update if newSchema.Schema != "" { - gqlSchema, err = generateGQLSchema(newSchema) + gqlSchema, err = generateGQLSchema(newSchema, ns) if err != nil { glog.Errorf("Error processing GraphQL schema: %s. ", err) return @@ -659,13 +659,13 @@ func getCurrentGraphQLSchema(namespace uint64) (*gqlSchema, error) { return &gqlSchema{ID: uid, Schema: graphQLSchema}, nil } -func generateGQLSchema(sch *gqlSchema) (schema.Schema, error) { +func generateGQLSchema(sch *gqlSchema, ns uint64) (schema.Schema, error) { schHandler, err := schema.NewHandler(sch.Schema, false) if err != nil { return nil, err } sch.GeneratedSchema = schHandler.GQLSchema() - generatedSchema, err := schema.FromString(sch.GeneratedSchema) + generatedSchema, err := schema.FromString(sch.GeneratedSchema, ns) if err != nil { return nil, err } @@ -709,7 +709,7 @@ func (as *adminServer) initServer() { break } - generatedSchema, err := generateGQLSchema(sch) + generatedSchema, err := generateGQLSchema(sch, x.GalaxyNamespace) if err != nil { glog.Infof("Error processing GraphQL schema: %s.", err) break @@ -818,7 +818,7 @@ func (as *adminServer) resetSchema(ns uint64, gqlSchema schema.Schema) { // introspection operations, and set GQL schema to empty. if gqlSchema == nil { resolverFactory = resolverFactoryWithErrorMsg(errNoGraphQLSchema) - gqlSchema, _ = schema.FromString("") + gqlSchema, _ = schema.FromString("", ns) } else { resolverFactory = resolverFactoryWithErrorMsg(errResolverNotFound). WithConventionResolvers(gqlSchema, as.fns) @@ -874,7 +874,7 @@ func (as *adminServer) lazyLoadSchema(namespace uint64) { return } - generatedSchema, err := generateGQLSchema(sch) + generatedSchema, err := generateGQLSchema(sch, namespace) if err != nil { glog.Infof("Error processing GraphQL schema: %s.", err) return diff --git a/graphql/admin/current_user.go b/graphql/admin/current_user.go index 6bbcc43ee13..62bae249f13 100644 --- a/graphql/admin/current_user.go +++ b/graphql/admin/current_user.go @@ -35,7 +35,7 @@ func extractName(ctx context.Context) (string, error) { return "", err } - return x.ExtractUserName(accessJwt[0]) + return x.ExtractUserName(accessJwt) } func (gsr *currentUserResolver) Rewrite(ctx context.Context, diff --git a/graphql/admin/schema.go b/graphql/admin/schema.go index e940fbe4729..e2e944593c0 100644 --- a/graphql/admin/schema.go +++ b/graphql/admin/schema.go @@ -55,7 +55,8 @@ func (usr *updateSchemaResolver) Resolve(ctx context.Context, m schema.Mutation) return resolve.EmptyResult(m, err), false } - if _, err = schema.FromString(schHandler.GQLSchema()); err != nil { + // we don't need the correct namespace for validation, so passing the Galaxy namespace + if _, err = schema.FromString(schHandler.GQLSchema(), x.GalaxyNamespace); err != nil { return resolve.EmptyResult(m, err), false } diff --git a/graphql/admin/update_group.go b/graphql/admin/update_group.go index 946667854fb..80328184d5e 100644 --- a/graphql/admin/update_group.go +++ b/graphql/admin/update_group.go @@ -155,6 +155,13 @@ func (urw *updateGroupRewriter) FromMutationResult( return ((*resolve.UpdateRewriter)(urw)).FromMutationResult(ctx, mutation, assigned, result) } +func (urw *updateGroupRewriter) MutatedRootUIDs( + mutation schema.Mutation, + assigned map[string]string, + result map[string]interface{}) []string { + return ((*resolve.UpdateRewriter)(urw)).MutatedRootUIDs(mutation, assigned, result) +} + // addAclRuleQuery adds a *gql.GraphQuery to upsertQuery.Children to query a rule inside a group // based on its predicate value. func addAclRuleQuery(upsertQuery []*gql.GraphQuery, predicate, variable string) { diff --git a/graphql/dgraph/execute.go b/graphql/dgraph/execute.go index 027d9f3bb41..34e7278afa9 100644 --- a/graphql/dgraph/execute.go +++ b/graphql/dgraph/execute.go @@ -64,7 +64,7 @@ func (dg *DgraphEx) Execute(ctx context.Context, req *dgoapi.Request, } // CommitOrAbort is the underlying dgraph implementation for committing a Dgraph transaction -func (dg *DgraphEx) CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error { - _, err := (&edgraph.Server{}).CommitOrAbort(ctx, tc) - return err +func (dg *DgraphEx) CommitOrAbort(ctx context.Context, + tc *dgoapi.TxnContext) (*dgoapi.TxnContext, error) { + return (&edgraph.Server{}).CommitOrAbort(ctx, tc) } diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index 42f082f6392..94cde87b4a0 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -51,6 +51,9 @@ var ( dgraphHealthURL = "http://" + Alpha1HTTP + "/health?all" dgraphStateURL = "http://" + Alpha1HTTP + "/state" + // this port is used on the host machine to spin up a test HTTP server + lambdaHookServerAddr = ":8888" + retryableUpdateGQLSchemaErrors = []string{ "errIndexingInProgress", "is already running", @@ -797,8 +800,8 @@ func RunAll(t *testing.T) { t.Run("query only typename", queryOnlyTypename) t.Run("query nested only typename", querynestedOnlyTypename) t.Run("test onlytypename for interface types", onlytypenameForInterface) - t.Run("entitites Query on extended type with key field of type String", entitiesQueryWithKeyFieldOfTypeString) - t.Run("entitites Query on extended type with key field of type Int", entitiesQueryWithKeyFieldOfTypeInt) + t.Run("entities Query on extended type with key field of type String", entitiesQueryWithKeyFieldOfTypeString) + t.Run("entities Query on extended type with key field of type Int", entitiesQueryWithKeyFieldOfTypeInt) t.Run("get state by xid", getStateByXid) t.Run("get state without args", getStateWithoutArgs) @@ -915,6 +918,7 @@ func RunAll(t *testing.T) { t.Run("lambda on mutation using graphql", lambdaOnMutationUsingGraphQL) t.Run("query lambda field in a mutation with duplicate @id", lambdaInMutationWithDuplicateId) t.Run("lambda with apollo federation", lambdaWithApolloFederation) + t.Run("lambdaOnMutate hooks", lambdaOnMutateHooks) } func gunzipData(data []byte) ([]byte, error) { diff --git a/graphql/e2e/common/error.go b/graphql/e2e/common/error.go index 7b67fcf6fb5..47d39c3486d 100644 --- a/graphql/e2e/common/error.go +++ b/graphql/e2e/common/error.go @@ -319,8 +319,9 @@ func (dg *panicClient) Execute(ctx context.Context, req *dgoapi.Request, return nil, nil } -func (dg *panicClient) CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error { - return nil +func (dg *panicClient) CommitOrAbort(ctx context.Context, + tc *dgoapi.TxnContext) (*dgoapi.TxnContext, error) { + return &dgoapi.TxnContext{}, nil } // clientInfoLogin check whether the client info(IP address) is propagated in the request. diff --git a/graphql/e2e/common/lambda.go b/graphql/e2e/common/lambda.go index 1acd33842b5..5648331f7a1 100644 --- a/graphql/e2e/common/lambda.go +++ b/graphql/e2e/common/lambda.go @@ -17,8 +17,14 @@ package common import ( + "context" "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" "testing" + "time" "github.com/stretchr/testify/require" @@ -339,3 +345,170 @@ func lambdaWithApolloFederation(t *testing.T) { DeleteGqlType(t, "Astronaut", map[string]interface{}{"id": []interface{}{"14", "30", "7"}}, 3, nil) } + +// TODO(GRAPHQL-1123): need to find a way to make it work on TeamCity machines. +// The host `172.17.0.1` used to connect to host machine from within docker, doesn't seem to +// work in teamcity machines, neither does `host.docker.internal` works there. So, we are +// skipping the related test for now. +func lambdaOnMutateHooks(t *testing.T) { + t.Skipf("can't reach host machine from within docker") + // let's listen to the changes coming in from the lambda hook and store them in this array + var changelog []string + server := http.Server{Addr: lambdaHookServerAddr, Handler: http.NewServeMux()} + defer server.Shutdown(context.Background()) + go func() { + serverMux := server.Handler.(*http.ServeMux) + serverMux.HandleFunc("/changelog", func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + b, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + + var event map[string]interface{} + require.NoError(t, json.Unmarshal(b, &event)) + require.Greater(t, event["commitTs"], float64(0)) + delete(event, "commitTs") + + b, err = json.Marshal(event) + require.NoError(t, err) + + changelog = append(changelog, string(b)) + }) + t.Log(server.ListenAndServe()) + }() + + // wait a bit to make sure the server has started + time.Sleep(2 * time.Second) + + // 1. Add 2 districts: D1, D2 + addDistrictParams := &GraphQLParams{ + Query: `mutation ($input: [AddDistrictInput!]!, $upsert: Boolean){ + addDistrict(input: $input, upsert: $upsert) { + district { + dgId + id + } + } + }`, + Variables: map[string]interface{}{ + "input": []interface{}{ + map[string]interface{}{"id": "D1", "name": "Dist-1"}, + map[string]interface{}{"id": "D2", "name": "Dist-2"}, + }, + "upsert": false, + }, + } + resp := addDistrictParams.ExecuteAsPost(t, GraphqlURL) + resp.RequireNoGQLErrors(t) + + var addResp struct { + AddDistrict struct{ District []struct{ DgId, Id string } } + } + require.NoError(t, json.Unmarshal(resp.Data, &addResp)) + require.Len(t, addResp.AddDistrict.District, 2) + + // find the uid for each district, to be used later in comparing expectation with reality + var d1Uid, d2Uid string + for _, dist := range addResp.AddDistrict.District { + switch dist.Id { + case "D1": + d1Uid = dist.DgId + case "D2": + d2Uid = dist.DgId + } + } + + // 2. Upsert the district D1 with an updated name + addDistrictParams.Variables = map[string]interface{}{ + "input": []interface{}{ + map[string]interface{}{"id": "D1", "name": "Dist_1"}, + }, + "upsert": true, + } + resp = addDistrictParams.ExecuteAsPost(t, GraphqlURL) + resp.RequireNoGQLErrors(t) + + // 3. Update the name for district D2 + updateDistrictParams := &GraphQLParams{ + Query: `mutation { + updateDistrict(input: { + filter: { id: {eq: "D2"}} + set: {name: "Dist_2"} + remove: {name: "Dist-2"} + }) { + numUids + } + }`, + } + resp = updateDistrictParams.ExecuteAsPost(t, GraphqlURL) + resp.RequireNoGQLErrors(t) + + // 4. Delete both the Districts + DeleteGqlType(t, "District", GetXidFilter("id", []interface{}{"D1", "D2"}), 2, nil) + + // let's wait for at least 5 secs to get all the updates from the lambda hook + time.Sleep(5 * time.Second) + + // compare the expected vs the actual ones + testutil.CompareJSON(t, fmt.Sprintf(`{"changelog": [ + { + "__typename": "District", + "operation": "add", + "add": { + "rootUIDs": [ + "%s", + "%s" + ], + "input": [ + { + "id": "D1", + "name": "Dist-1" + }, + { + "id": "D2", + "name": "Dist-2" + } + ] + } + }, + { + "__typename": "District", + "operation": "add", + "add": { + "rootUIDs": [ + "%s" + ], + "input": [ + { + "name": "Dist_1" + } + ] + } + }, + { + "__typename": "District", + "operation": "update", + "update": { + "rootUIDs": [ + "%s" + ], + "setPatch": { + "name": "Dist_2" + }, + "removePatch": { + "name": "Dist-2" + } + } + }, + { + "__typename": "District", + "operation": "delete", + "delete": { + "rootUIDs": [ + "%s", + "%s" + ] + } + } + ]}`, d1Uid, d2Uid, d1Uid, d2Uid, d1Uid, d2Uid), + `{"changelog": [`+strings.Join(changelog, ",")+"]}") +} diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index c8ad6011751..f6a1ce0d265 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -341,7 +341,8 @@ type Region { district: District } -type District { +type District @lambdaOnMutate(add: true, update: true, delete: true) { + dgId: ID! id: String! @id name: String! } diff --git a/graphql/e2e/directives/script.js b/graphql/e2e/directives/script.js index ce40404f03b..fd9116645d8 100644 --- a/graphql/e2e/directives/script.js +++ b/graphql/e2e/directives/script.js @@ -52,4 +52,20 @@ async function rank({parents}) { self.addMultiParentGraphQLResolvers({ "Author.rank": rank -}) \ No newline at end of file +}) + +async function districtWebhook({ dql, graphql, authHeader, event }) { + // forward the event to the changelog server running on the host machine + await fetch(`http://172.17.0.1:8888/changelog`, { + method: "POST", + body: JSON.stringify(event) + }) + // just return, nothing else to do with response +} + +self.addWebHookResolvers({ + "District.add": districtWebhook, + "District.update": districtWebhook, + "District.delete": districtWebhook, +}) + diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index bba11149583..68ee2d21568 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -360,7 +360,8 @@ type Region { district: District } -type District { +type District @lambdaOnMutate(add: true, update: true, delete: true) { + dgId: ID! id: String! @id name: String! } diff --git a/graphql/e2e/normal/script.js b/graphql/e2e/normal/script.js index 3209a609fef..1077c1b2a36 100644 --- a/graphql/e2e/normal/script.js +++ b/graphql/e2e/normal/script.js @@ -52,4 +52,24 @@ async function rank({parents}) { self.addMultiParentGraphQLResolvers({ "Author.rank": rank -}) \ No newline at end of file +}) + +// TODO(GRAPHQL-1123): need to find a way to make it work on TeamCity machines. +// The host `172.17.0.1` used to connect to host machine from within docker, doesn't seem to +// work in teamcity machines, neither does `host.docker.internal` works there. So, we are +// skipping the related test for now. +async function districtWebhook({ dql, graphql, authHeader, event }) { + // forward the event to the changelog server running on the host machine + await fetch(`http://172.17.0.1:8888/changelog`, { + method: "POST", + body: JSON.stringify(event) + }) + // just return, nothing else to do with response +} + +self.addWebHookResolvers({ + "District.add": districtWebhook, + "District.update": districtWebhook, + "District.delete": districtWebhook, +}) + diff --git a/graphql/e2e/schema/apollo_service_response.graphql b/graphql/e2e/schema/apollo_service_response.graphql index 1f0f8d4d506..854913f071c 100644 --- a/graphql/e2e/schema/apollo_service_response.graphql +++ b/graphql/e2e/schema/apollo_service_response.graphql @@ -204,6 +204,7 @@ directive @secret(field: String!, pred: String) on OBJECT | INTERFACE directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE input IntFilter { eq: Int diff --git a/graphql/e2e/schema/generatedSchema.graphql b/graphql/e2e/schema/generatedSchema.graphql index 53da0729dd8..26c14b07a4a 100644 --- a/graphql/e2e/schema/generatedSchema.graphql +++ b/graphql/e2e/schema/generatedSchema.graphql @@ -193,6 +193,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/resolve/auth_test.go b/graphql/resolve/auth_test.go index 081a9c71e0c..a08bc26324d 100644 --- a/graphql/resolve/auth_test.go +++ b/graphql/resolve/auth_test.go @@ -165,8 +165,9 @@ func (ex *authExecutor) Execute(ctx context.Context, req *dgoapi.Request, panic("test failed") } -func (ex *authExecutor) CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error { - return nil +func (ex *authExecutor) CommitOrAbort(ctx context.Context, + tc *dgoapi.TxnContext) (*dgoapi.TxnContext, error) { + return &dgoapi.TxnContext{}, nil } func TestStringCustomClaim(t *testing.T) { diff --git a/graphql/resolve/mutation.go b/graphql/resolve/mutation.go index 20847300c5e..cba9b50ef91 100644 --- a/graphql/resolve/mutation.go +++ b/graphql/resolve/mutation.go @@ -112,6 +112,11 @@ type MutationRewriter interface { m schema.Mutation, assigned map[string]string, result map[string]interface{}) ([]*gql.GraphQuery, error) + // MutatedRootUIDs returns a list of Root UIDs that were mutated as part of the mutation. + MutatedRootUIDs( + mutation schema.Mutation, + assigned map[string]string, + result map[string]interface{}) []string } // A DgraphExecutor can execute a query/mutation and returns the request response and any errors. @@ -120,7 +125,7 @@ type DgraphExecutor interface { // occurs, that indicates that the execution failed in some way significant enough // way as to not continue processing this query/mutation or others in the same request. Execute(ctx context.Context, req *dgoapi.Request, field schema.Field) (*dgoapi.Response, error) - CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error + CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) (*dgoapi.TxnContext, error) } // An UpsertMutation is the query and mutations needed for a Dgraph upsert. @@ -215,9 +220,9 @@ func (mr *dgraphResolver) rewriteAndExecute( defer func() { if !commit && mutResp != nil && mutResp.Txn != nil { mutResp.Txn.Aborted = true - err := mr.executor.CommitOrAbort(ctx, mutResp.Txn) + _, err := mr.executor.CommitOrAbort(ctx, mutResp.Txn) if err != nil { - glog.Errorf("Error occured while aborting transaction: %s", err) + glog.Errorf("Error occurred while aborting transaction: %s", err) } } }() @@ -414,7 +419,7 @@ func (mr *dgraphResolver) rewriteAndExecute( return emptyResult(queryErrs), resolverFailed } - err = mr.executor.CommitOrAbort(ctx, mutResp.Txn) + txnCtx, err := mr.executor.CommitOrAbort(ctx, mutResp.Txn) if err != nil { return emptyResult( schema.GQLWrapf(err, "mutation failed, couldn't commit transaction")), @@ -422,6 +427,12 @@ func (mr *dgraphResolver) rewriteAndExecute( } commit = true + // once committed, send async updates to configured webhooks, if any. + if mutation.HasLambdaOnMutate() { + rootUIDs := mr.mutationRewriter.MutatedRootUIDs(mutation, mutResp.GetUids(), result) + go sendWebhookEvent(ctx, mutation, txnCtx.CommitTs, rootUIDs) + } + // For delete mutation, we would have already populated qryResp if query field was requested. if mutation.MutationType() != schema.DeleteMutation { queryTimer := newtimer(ctx, &dgraphQueryDuration.OffsetDuration) diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index 7ee299d94a8..d0a4b67da0f 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -263,12 +263,12 @@ func (xidMetadata *xidMetadata) isDuplicateXid(atTopLevel bool, xidVar string, // simply ignored. // If it is found out that the Person with id 0x123 does not exist, the corresponding // mutation will fail. -func (mrw *AddRewriter) RewriteQueries( +func (arw *AddRewriter) RewriteQueries( ctx context.Context, m schema.Mutation) ([]*gql.GraphQuery, error) { - mrw.VarGen = NewVariableGenerator() - mrw.XidMetadata = NewXidMetadata() + arw.VarGen = NewVariableGenerator() + arw.XidMetadata = NewXidMetadata() mutatedType := m.MutatedType() val, _ := m.ArgValue(schema.InputArgName).([]interface{}) @@ -278,7 +278,7 @@ func (mrw *AddRewriter) RewriteQueries( for _, i := range val { obj := i.(map[string]interface{}) - queries, errs := existenceQueries(ctx, mutatedType, nil, mrw.VarGen, obj, mrw.XidMetadata) + queries, errs := existenceQueries(ctx, mutatedType, nil, arw.VarGen, obj, arw.XidMetadata) if len(errs) > 0 { var gqlErrors x.GqlErrorList for _, err := range errs { @@ -412,7 +412,7 @@ func (urw *UpdateRewriter) RewriteQueries( // } ], // "Author.friends":[ {"uid":"0x123"} ], // } -func (mrw *AddRewriter) Rewrite( +func (arw *AddRewriter) Rewrite( ctx context.Context, m schema.Mutation, idExistence map[string]string) ([]*UpsertMutation, error) { @@ -421,8 +421,8 @@ func (mrw *AddRewriter) Rewrite( mutatedType := m.MutatedType() val, _ := m.ArgValue(schema.InputArgName).([]interface{}) - varGen := mrw.VarGen - xidMetadata := mrw.XidMetadata + varGen := arw.VarGen + xidMetadata := arw.XidMetadata // ret stores a slice of Upsert Mutations. These are used in executing upsert queries in graphql/resolve/mutation.go var ret []*UpsertMutation // fragments stores a slice of mutationFragments. This is used in constructing mutationsAll which is returned back to the caller @@ -490,7 +490,7 @@ func (mrw *AddRewriter) Rewrite( } if fragment != nil { fragments = append(fragments, fragment) - mrw.frags = append(mrw.frags, []*mutationFragment{fragment}) + arw.frags = append(arw.frags, []*mutationFragment{fragment}) } } @@ -720,7 +720,7 @@ func (urw *UpdateRewriter) Rewrite( } // FromMutationResult rewrites the query part of a GraphQL add mutation into a Dgraph query. -func (mrw *AddRewriter) FromMutationResult( +func (arw *AddRewriter) FromMutationResult( ctx context.Context, mutation schema.Mutation, assigned map[string]string, @@ -728,46 +728,15 @@ func (mrw *AddRewriter) FromMutationResult( var errs error - // This stores a list of added or updated uids. - uids := make([]uint64, 0) - - // Add any newly added uids. - for _, frag := range mrw.frags { + for _, frag := range arw.frags { err := checkResult(frag, result) errs = schema.AppendGQLErrs(errs, err) - if err != nil { - continue - } - - node := strings.TrimPrefix(frag[0]. - fragment.(map[string]interface{})["uid"].(string), "_:") - val, ok := assigned[node] - if !ok { - // node was not part of assigned map. It is likely going to be part of Updated UIDs map. - // Extract and add any updated uids. This is done for upsert With Add Mutation. - // We extract out the variable name, eg. Project1 from uid(Project1) - uidVar := frag[0].fragment.(map[string]interface{})["uid"].(string) - uidVar = strings.TrimPrefix(uidVar, "uid(") - uidVar = strings.TrimSuffix(uidVar, ")") - updated := extractMutated(result, uidVar) - updateUids, err := extractUidsFromMutated(updated) - if err != nil { - return nil, err - } - uids = append(uids, updateUids...) - continue - } - uid, err := strconv.ParseUint(val, 0, 64) - if err != nil { - errs = schema.AppendGQLErrs(errs, schema.GQLWrapf(err, - "received %s as an assigned uid from Dgraph,"+ - " but couldn't parse it as uint64", - assigned[node])) - } - - uids = append(uids, uid) } + // Find any newly added/updated rootUIDs. + uids, err := convertIDsWithErr(arw.MutatedRootUIDs(mutation, assigned, result)) + errs = schema.AppendGQLErrs(errs, err) + // Find out if its an upsert with Add mutation. // In this case, it may happen that no new node is created, but there may still // be some updated nodes. We don't throw an error in this case. @@ -816,9 +785,7 @@ func (urw *UpdateRewriter) FromMutationResult( return nil, err } - mutated := extractMutated(result, mutation.Name()) - - uids, err := extractUidsFromMutated(mutated) + uids, err := convertIDsWithErr(urw.MutatedRootUIDs(mutation, assigned, result)) if err != nil { return nil, err } @@ -838,6 +805,40 @@ func (urw *UpdateRewriter) FromMutationResult( return rewriteAsQueryByIds(mutation.QueryField(), uids, authRw), nil } +func (arw *AddRewriter) MutatedRootUIDs( + mutation schema.Mutation, + assigned map[string]string, + result map[string]interface{}) []string { + + var rootUIDs []string // This stores a list of added or updated rootUIDs. + + for _, frag := range arw.frags { + fragUid := frag[0].fragment.(map[string]interface{})["uid"].(string) + blankNodeName := strings.TrimPrefix(fragUid, "_:") + uid, ok := assigned[blankNodeName] + if ok { + // any newly added uids will be present in assigned map + rootUIDs = append(rootUIDs, uid) + } else { + // node was not part of assigned map. It is likely going to be part of Updated UIDs map. + // Extract and add any updated uids. This is done for upsert With Add Mutation. + // We extract out the variable name, eg. Project1 from uid(Project1) + uidVar := strings.TrimSuffix(strings.TrimPrefix(fragUid, "uid("), ")") + rootUIDs = append(rootUIDs, extractMutated(result, uidVar)...) + } + } + + return rootUIDs +} + +func (urw *UpdateRewriter) MutatedRootUIDs( + mutation schema.Mutation, + assigned map[string]string, + result map[string]interface{}) []string { + + return extractMutated(result, mutation.Name()) +} + func extractMutated(result map[string]interface{}, mutatedField string) []string { var mutated []string @@ -853,18 +854,20 @@ func extractMutated(result map[string]interface{}, mutatedField string) []string return mutated } -func extractUidsFromMutated(mutated []string) ([]uint64, error) { - var ret []uint64 - for _, id := range mutated { +// convertIDsWithErr is similar to convertIDs, except that it also returns the errors, if any. +func convertIDsWithErr(uidSlice []string) ([]uint64, error) { + var errs error + ret := make([]uint64, 0, len(uidSlice)) + for _, id := range uidSlice { uid, err := strconv.ParseUint(id, 0, 64) if err != nil { - return ret, schema.GQLWrapf(err, - "received %s as an updated uid from Dgraph, but couldn't parse it as "+ - "uint64", id) + errs = schema.AppendGQLErrs(errs, schema.GQLWrapf(err, + "received %s as a uid from Dgraph, but couldn't parse it as uint64", id)) + continue } ret = append(ret, uid) } - return ret, nil + return ret, errs } func addUpdateCondition(frags []*mutationFragment) { @@ -1123,6 +1126,14 @@ func (drw *deleteRewriter) FromMutationResult( return nil, nil } +func (drw *deleteRewriter) MutatedRootUIDs( + mutation schema.Mutation, + assigned map[string]string, + result map[string]interface{}) []string { + + return extractMutated(result, mutation.Name()) +} + // RewriteQueries on deleteRewriter does not return any queries. queries to check // existence of nodes are not needed as part of Delete Mutation. // The function generates VarGen and XidMetadata which are used in Rewrite function. diff --git a/graphql/resolve/resolver.go b/graphql/resolve/resolver.go index f069a3955f1..1c3811b3c47 100644 --- a/graphql/resolve/resolver.go +++ b/graphql/resolve/resolver.go @@ -178,7 +178,8 @@ func (aex *adminExecutor) Execute(ctx context.Context, req *dgoapi.Request, fiel return aex.dg.Execute(ctx, req, field) } -func (aex *adminExecutor) CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error { +func (aex *adminExecutor) CommitOrAbort(ctx context.Context, + tc *dgoapi.TxnContext) (*dgoapi.TxnContext, error) { return aex.dg.CommitOrAbort(ctx, tc) } @@ -187,7 +188,8 @@ func (de *dgraphExecutor) Execute(ctx context.Context, req *dgoapi.Request, fiel return de.dg.Execute(ctx, req, field) } -func (de *dgraphExecutor) CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error { +func (de *dgraphExecutor) CommitOrAbort(ctx context.Context, + tc *dgoapi.TxnContext) (*dgoapi.TxnContext, error) { return de.dg.CommitOrAbort(ctx, tc) } diff --git a/graphql/resolve/resolver_error_test.go b/graphql/resolve/resolver_error_test.go index 069afd4cfd3..3a0b3b29764 100644 --- a/graphql/resolve/resolver_error_test.go +++ b/graphql/resolve/resolver_error_test.go @@ -131,8 +131,9 @@ func (ex *executor) Execute(ctx context.Context, req *dgoapi.Request, } -func (ex *executor) CommitOrAbort(ctx context.Context, tc *dgoapi.TxnContext) error { - return nil +func (ex *executor) CommitOrAbort(ctx context.Context, + tc *dgoapi.TxnContext) (*dgoapi.TxnContext, error) { + return &dgoapi.TxnContext{}, nil } func complete(t *testing.T, gqlSchema schema.Schema, gqlQuery, dgResponse string) *schema.Response { diff --git a/graphql/resolve/webhook.go b/graphql/resolve/webhook.go new file mode 100644 index 00000000000..d641fe5f20c --- /dev/null +++ b/graphql/resolve/webhook.go @@ -0,0 +1,130 @@ +/* + * Copyright 2021 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 resolve + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/golang/glog" + "github.com/pkg/errors" + + "github.com/dgraph-io/dgraph/graphql/authorization" + "github.com/dgraph-io/dgraph/graphql/schema" + "github.com/dgraph-io/dgraph/x" +) + +type webhookPayload struct { + Resolver string `json:"resolver"` + AccessJWT string `json:"X-Dgraph-AccessToken,omitempty"` + AuthHeader *authHeaderPayload `json:"authHeader,omitempty"` + Event eventPayload `json:"event"` +} + +type authHeaderPayload struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type eventPayload struct { + Typename string `json:"__typename"` + Operation schema.MutationType `json:"operation"` + CommitTs uint64 `json:"commitTs"` + Add *addEvent `json:"add,omitempty"` + Update *updateEvent `json:"update,omitempty"` + Delete *deleteEvent `json:"delete,omitempty"` +} + +type addEvent struct { + RootUIDs []string `json:"rootUIDs"` + Input []interface{} `json:"input"` +} + +type updateEvent struct { + RootUIDs []string `json:"rootUIDs"` + SetPatch interface{} `json:"setPatch"` + RemovePatch interface{} `json:"removePatch"` +} + +type deleteEvent struct { + RootUIDs []string `json:"rootUIDs"` +} + +// sendWebhookEvent forms an HTTP payload required for the webhooks configured with @lambdaOnMutate +// directive, and then sends that payload to the lambda URL configured with Alpha. There is no +// guarantee that the payload will be delivered successfully to the lambda server. +func sendWebhookEvent(ctx context.Context, m schema.Mutation, commitTs uint64, rootUIDs []string) { + accessJWT, _ := x.ExtractJwt(ctx) + var authHeader *authHeaderPayload + if m.GetAuthMeta() != nil { + authHeader = &authHeaderPayload{ + Key: m.GetAuthMeta().GetHeader(), + Value: authorization.GetJwtToken(ctx), + } + } + + payload := webhookPayload{ + Resolver: "$webhook", + AccessJWT: accessJWT, + AuthHeader: authHeader, + Event: eventPayload{ + Typename: m.MutatedType().Name(), + Operation: m.MutationType(), + CommitTs: commitTs, + }, + } + + switch payload.Event.Operation { + case schema.AddMutation: + input, _ := m.ArgValue(schema.InputArgName).([]interface{}) + payload.Event.Add = &addEvent{ + RootUIDs: rootUIDs, + Input: input, + } + case schema.UpdateMutation: + inp, _ := m.ArgValue(schema.InputArgName).(map[string]interface{}) + payload.Event.Update = &updateEvent{ + RootUIDs: rootUIDs, + SetPatch: inp["set"], + RemovePatch: inp["remove"], + } + case schema.DeleteMutation: + payload.Event.Delete = &deleteEvent{RootUIDs: rootUIDs} + } + + b, err := json.Marshal(payload) + if err != nil { + glog.Error(errors.Wrap(err, "error marshalling webhook payload")) + // don't care to send the payload if there are JSON marshalling errors + return + } + + // send the request + ns, _ := x.ExtractNamespace(ctx) + headers := http.Header{} + headers.Set("Content-Type", "application/json") + resp, err := schema.MakeHttpRequest(nil, http.MethodPost, x.LambdaUrl(ns), headers, b) + + // just log the response errors, if any. + if err != nil { + glog.V(3).Info(errors.Wrap(err, "unable to send webhook event")) + } + if resp != nil && (resp.StatusCode < 200 || resp.StatusCode >= 300) { + glog.V(3).Info(errors.Errorf("got unsuccessful status from webhook: %s", resp.Status)) + } +} diff --git a/graphql/schema/custom_http.go b/graphql/schema/custom_http.go index a58802ad8c9..e3794666472 100644 --- a/graphql/schema/custom_http.go +++ b/graphql/schema/custom_http.go @@ -41,11 +41,11 @@ type graphqlResp struct { Data map[string]interface{} `json:"data,omitempty"` } -// makeHttpRequest sends an HTTP request using the provided inputs. It returns the response body -// and status code along with any errors that were encountered. +// MakeHttpRequest sends an HTTP request using the provided inputs. It returns the HTTP response +// along with any errors that were encountered. // If no client is provided, it uses the defaultHttpClient which has a timeout of 1 minute. -func makeHttpRequest(client *http.Client, method, url string, header http.Header, - body []byte) ([]byte, int, error) { +func MakeHttpRequest(client *http.Client, method, url string, header http.Header, + body []byte) (*http.Response, error) { var reqBody io.Reader if len(body) == 0 { reqBody = http.NoBody @@ -55,22 +55,14 @@ func makeHttpRequest(client *http.Client, method, url string, header http.Header req, err := http.NewRequest(method, url, reqBody) if err != nil { - return nil, http.StatusBadRequest, err + return nil, err } req.Header = header if client == nil { client = defaultHttpClient } - resp, err := client.Do(req) - if err != nil { - return nil, http.StatusBadRequest, err - } - - defer resp.Body.Close() - b, err := ioutil.ReadAll(resp.Body) - - return b, resp.StatusCode, err + return client.Do(req) } // MakeAndDecodeHTTPRequest sends an HTTP request using the given url and body and then decodes the @@ -94,7 +86,13 @@ func (fconf *FieldHTTPConfig) MakeAndDecodeHTTPRequest(client *http.Client, url } // Make the request to external HTTP endpoint using the URL and body - b, statusCode, err := makeHttpRequest(client, fconf.Method, url, fconf.ForwardHeaders, b) + resp, err := MakeHttpRequest(client, fconf.Method, url, fconf.ForwardHeaders, b) + if err != nil { + return nil, nil, x.GqlErrorList{externalRequestError(err, field)} + } + + defer resp.Body.Close() + b, err = ioutil.ReadAll(resp.Body) if err != nil { return nil, nil, x.GqlErrorList{externalRequestError(err, field)} } @@ -118,7 +116,7 @@ func (fconf *FieldHTTPConfig) MakeAndDecodeHTTPRequest(client *http.Client, url } } else { // this was a REST request - if statusCode >= 200 && statusCode < 300 { + if resp.StatusCode >= 200 && resp.StatusCode < 300 { // if this was a successful request, lets try to unmarshal the response if err = Unmarshal(b, &response); err != nil { return nil, nil, x.GqlErrorList{jsonUnmarshalError(err, field)} @@ -128,7 +126,7 @@ func (fconf *FieldHTTPConfig) MakeAndDecodeHTTPRequest(client *http.Client, url // if we get unsuccessful response from the REST api, lets try to see if // it sent any errors in the form expected for GraphQL errors. if err = Unmarshal(b, &graphqlResp); err != nil { - err = fmt.Errorf("unexpected error with: %v", statusCode) + err = fmt.Errorf("unexpected error with: %v", resp.StatusCode) return nil, nil, x.GqlErrorList{externalRequestError(err, field)} } else { return nil, nil, graphqlResp.Errors @@ -164,11 +162,14 @@ func externalRequestError(err error, f Field) *x.GqlError { func GetBodyForLambda(ctx context.Context, field Field, parents, args interface{}) map[string]interface{} { - body := make(map[string]interface{}) - body["resolver"] = field.GetObjectName() + "." + field.Name() - body["authHeader"] = map[string]interface{}{ - "key": field.GetAuthMeta().GetHeader(), - "value": authorization.GetJwtToken(ctx), + accessJWT, _ := x.ExtractJwt(ctx) + body := map[string]interface{}{ + "resolver": field.GetObjectName() + "." + field.Name(), + "X-Dgraph-AccessToken": accessJWT, + "authHeader": map[string]interface{}{ + "key": field.GetAuthMeta().GetHeader(), + "value": authorization.GetJwtToken(ctx), + }, } if parents != nil { body["parents"] = parents diff --git a/graphql/schema/dgraph_schemagen_test.yml b/graphql/schema/dgraph_schemagen_test.yml index 95637962543..f17ee00baf5 100644 --- a/graphql/schema/dgraph_schemagen_test.yml +++ b/graphql/schema/dgraph_schemagen_test.yml @@ -855,3 +855,15 @@ schemas: User.username: string . User.reviews: [uid] . + - name: "nothing is added in dgraph schema with lambdaOnMutate" + input: | + type T @lambdaOnMutate(add: true, update: true, delete: true) { + id : ID! + value: String + } + output: | + type T { + T.value + } + T.value: string . + diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index 0f4f3f954e1..bc8c4cc2744 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -46,6 +46,7 @@ const ( remoteDirective = "remote" // types with this directive are not stored in Dgraph. remoteResponseDirective = "remoteResponse" lambdaDirective = "lambda" + lambdaOnMutateDirective = "lambdaOnMutate" generateDirective = "generate" generateQueryArg = "query" @@ -289,6 +290,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, @@ -311,6 +313,7 @@ directive @secret(field: String!, pred: String) on OBJECT | INTERFACE directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE ` filterInputs = ` input IntFilter { @@ -566,6 +569,7 @@ var directiveValidators = map[string]directiveValidator{ remoteDirective: ValidatorNoOp, deprecatedDirective: ValidatorNoOp, lambdaDirective: lambdaDirectiveValidation, + lambdaOnMutateDirective: ValidatorNoOp, generateDirective: ValidatorNoOp, apolloKeyDirective: ValidatorNoOp, apolloExtendsDirective: ValidatorNoOp, @@ -588,10 +592,16 @@ var directiveLocationMap = map[string]map[ast.DefinitionKind]bool{ customDirective: nil, remoteDirective: {ast.Object: true, ast.Interface: true, ast.Union: true, ast.InputObject: true, ast.Enum: true}, - cascadeDirective: nil, + lambdaDirective: nil, + lambdaOnMutateDirective: {ast.Object: true, ast.Interface: true}, generateDirective: {ast.Object: true, ast.Interface: true}, + apolloKeyDirective: {ast.Object: true, ast.Interface: true}, + apolloExtendsDirective: {ast.Object: true, ast.Interface: true}, + apolloExternalDirective: nil, apolloRequiresDirective: nil, apolloProvidesDirective: nil, + remoteResponseDirective: nil, + cascadeDirective: nil, } // Struct to store parameters of @generate directive diff --git a/graphql/schema/gqlschema_test.yml b/graphql/schema/gqlschema_test.yml index c132d81f1cc..d4c5f9f68a9 100644 --- a/graphql/schema/gqlschema_test.yml +++ b/graphql/schema/gqlschema_test.yml @@ -2852,6 +2852,34 @@ invalid_schemas: { "message": "Type TwitterUser: Field name: @remoteResponse directive can only be defined on fields of @remote type.", "locations": [{"line": 3, "column": 17}]} ] + - name: "@lambdaOnMutate with bad arg values" + input: | + type TwitterUser @lambdaOnMutate(add: true, update: badValue, delete: "false", badArg: true) { + id: ID! + name: String + } + errlist: [ + { "message": "Type TwitterUser; update argument in @lambdaOnMutate directive can only be true/false, found: `badValue`.", "locations": [{"line": 1, "column": 45}]}, + { "message": "Type TwitterUser; delete argument in @lambdaOnMutate directive can only be true/false, found: `\"false\"`.", "locations": [{"line": 1, "column": 63}]}, + { "message": "Type TwitterUser; @lambdaOnMutate directive doesn't support argument named: `badArg`.", "locations": [{"line": 1, "column": 80}]} + ] + + - name: "@lambdaOnMutate isn't allowed on @remote types" + input: | + type TwitterUser @remote @lambdaOnMutate(add: true) { + id: ID! + name: String + } + type Query{ + getCustomTwitterUser(name: String!): TwitterUser @custom(http:{ + url: "https://api.twitter.com/1.1/users/show.json?screen_name=$name" + method: "GET" + }) + } + errlist: [ + { "message": "Type TwitterUser; @lambdaOnMutate directive not allowed along with @remote directive.", "locations": [{"line": 1, "column": 27}]} + ] + valid_schemas: - name: "Multiple fields with @id directive should be allowed" input: | @@ -3316,3 +3344,17 @@ valid_schemas: reviews: [Review] } + + + - name: "@lambdaOnMutate is allowed on types and interfaces" + input: | + interface Post @lambdaOnMutate(add: true, delete: false) { + id: ID! + title: String + } + + type Question implements Post @lambdaOnMutate(add: true, update: true) { + id: ID! + questionText: String + } + diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index 0fc0a277e7c..a9453a30125 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -39,7 +39,8 @@ func init() { schemaValidations = append(schemaValidations, dgraphDirectivePredicateValidation) typeValidations = append(typeValidations, idCountCheck, dgraphDirectiveTypeValidation, passwordDirectiveValidation, conflictingDirectiveValidation, nonIdFieldsCheck, - remoteTypeValidation, generateDirectiveValidation, apolloKeyValidation, apolloExtendsValidation) + remoteTypeValidation, generateDirectiveValidation, apolloKeyValidation, + apolloExtendsValidation, lambdaOnMutateValidation) fieldValidations = append(fieldValidations, listValidityCheck, fieldArgumentCheck, fieldNameCheck, isValidFieldForList, hasAuthDirective, fieldDirectiveCheck) @@ -1231,7 +1232,7 @@ func lambdaDirectiveValidation(sch *ast.Schema, 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.GraphQL.GetString("lambda-url") == "" { + if x.LambdaUrl(x.GalaxyNamespace) == "" { 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.`, @@ -1239,13 +1240,62 @@ func lambdaDirectiveValidation(sch *ast.Schema, } // reuse @custom directive validation errs := customDirectiveValidation(sch, typ, field, buildCustomDirectiveForLambda(typ, field, - dir, func(f *ast.FieldDefinition) bool { return false }), secrets) + dir, x.GalaxyNamespace, func(f *ast.FieldDefinition) bool { return false }), secrets) for _, err := range errs { err.Message = "While building @custom for @lambda: " + err.Message } return errs } +func lambdaOnMutateValidation(sch *ast.Schema, typ *ast.Definition) gqlerror.List { + dir := typ.Directives.ForName(lambdaOnMutateDirective) + if dir == nil { + return nil + } + + var errs []*gqlerror.Error + + // lambda url must be specified during alpha startup + if x.LambdaUrl(x.GalaxyNamespace) == "" { + errs = append(errs, gqlerror.ErrorPosf(dir.Position, + "Type %s: has the @lambdaOnMutate directive, but the "+ + "`--graphql_lambda_url` flag wasn't specified during alpha startup.", typ.Name)) + } + + if typ.Directives.ForName(remoteDirective) != nil { + errs = append(errs, gqlerror.ErrorPosf( + dir.Position, + "Type %s; @lambdaOnMutate directive not allowed along with @remote directive.", + typ.Name)) + } + + for _, arg := range dir.Arguments { + switch arg.Name { + case "add": + case "update": + case "delete": + // do nothing + default: + errs = append(errs, gqlerror.ErrorPosf( + arg.Position, + "Type %s; @lambdaOnMutate directive doesn't support argument named: `%s`.", + typ.Name, arg.Name)) + continue // to next arg + } + + // validate add/update/delete args + if arg.Value.Kind != ast.BooleanValue { + errs = append(errs, gqlerror.ErrorPosf( + arg.Position, + "Type %s; %s argument in @lambdaOnMutate directive can only be "+ + "true/false, found: `%s`.", + typ.Name, arg.Name, arg.Value.String())) + } + } + + return errs +} + func generateDirectiveValidation(schema *ast.Schema, typ *ast.Definition) gqlerror.List { dir := typ.Directives.ForName(generateDirective) if dir == nil { diff --git a/graphql/schema/schemagen.go b/graphql/schema/schemagen.go index d28105c4ac1..60e1b0b0017 100644 --- a/graphql/schema/schemagen.go +++ b/graphql/schema/schemagen.go @@ -51,7 +51,7 @@ type handler struct { // FromString builds a GraphQL Schema from input string, or returns any parsing // or validation errors. -func FromString(schema string) (Schema, error) { +func FromString(schema string, ns uint64) (Schema, error) { // validator.Prelude includes a bunch of predefined types which help with schema introspection // queries, hence we include it as part of the schema. doc, gqlErr := parser.ParseSchemas(validator.Prelude, &ast.Source{Input: schema}) @@ -64,7 +64,7 @@ func FromString(schema string) (Schema, error) { return nil, errors.Wrap(gqlErr, "while validating GraphQL schema") } - return AsSchema(gqlSchema) + return AsSchema(gqlSchema, ns) } func (s *handler) MetaInfo() *metaInfo { @@ -187,7 +187,8 @@ type metaInfo struct { // allowedCorsOrigins stores allowed CORS origins extracted from # Dgraph.Allow-Origin. // They are returned to the client as part of Access-Control-Allow-Origin. allowedCorsOrigins map[string]bool - // authMeta stores the authorization meta info extracted from # Dgraph.Authorization + // authMeta stores the authorization meta info extracted from `# Dgraph.Authorization` if any, + // otherwise it is nil. authMeta *authorization.AuthMeta } diff --git a/graphql/schema/schemagen_test.go b/graphql/schema/schemagen_test.go index ce801f34079..94e0022f733 100644 --- a/graphql/schema/schemagen_test.go +++ b/graphql/schema/schemagen_test.go @@ -90,7 +90,7 @@ func TestSchemaString(t *testing.T) { newSchemaStr := schHandler.GQLSchema() - _, err = FromString(newSchemaStr) + _, err = FromString(newSchemaStr, x.GalaxyNamespace) require.NoError(t, err) outputFileName := outputDir + testFile.Name() str2, err := ioutil.ReadFile(outputFileName) @@ -121,7 +121,7 @@ func TestApolloServiceQueryResult(t *testing.T) { apolloServiceResult := schHandler.GQLSchemaWithoutApolloExtras() - _, err = FromString(schHandler.GQLSchema()) + _, err = FromString(schHandler.GQLSchema(), x.GalaxyNamespace) require.NoError(t, err) outputFileName := outputDir + testFile.Name() str2, err := ioutil.ReadFile(outputFileName) @@ -150,7 +150,7 @@ func TestSchemas(t *testing.T) { newSchemaStr := schHandler.GQLSchema() - _, err = FromString(newSchemaStr) + _, err = FromString(newSchemaStr, x.GalaxyNamespace) require.NoError(t, err) }) } @@ -159,7 +159,10 @@ func TestSchemas(t *testing.T) { t.Run("Invalid Schemas", func(t *testing.T) { for _, sch := range tests["invalid_schemas"] { t.Run(sch.Name, func(t *testing.T) { - _, errlist := NewHandler(sch.Input, false) + schHandler, errlist := NewHandler(sch.Input, false) + if errlist == nil { + _, errlist = FromString(schHandler.GQLSchema(), x.GalaxyNamespace) + } if diff := cmp.Diff(sch.Errlist, errlist, cmpopts.IgnoreUnexported(gqlerror.Error{})); diff != "" { t.Errorf("error mismatch (-want +got):\n%s", diff) } @@ -188,7 +191,7 @@ func TestAuthSchemas(t *testing.T) { schHandler, errlist := NewHandler(sch.Input, false) require.NoError(t, errlist, sch.Name) - _, authError := FromString(schHandler.GQLSchema()) + _, authError := FromString(schHandler.GQLSchema(), x.GalaxyNamespace) require.NoError(t, authError, sch.Name) }) } @@ -200,7 +203,7 @@ func TestAuthSchemas(t *testing.T) { schHandler, errlist := NewHandler(sch.Input, false) require.NoError(t, errlist, sch.Name) - _, authError := FromString(schHandler.GQLSchema()) + _, authError := FromString(schHandler.GQLSchema(), x.GalaxyNamespace) if diff := cmp.Diff(authError, sch.Errlist); diff != "" { t.Errorf("error mismatch (-want +got):\n%s", diff) diff --git a/graphql/schema/testdata/apolloservice/output/auth-directive.graphql b/graphql/schema/testdata/apolloservice/output/auth-directive.graphql index 50cfa934b31..f9402efd440 100644 --- a/graphql/schema/testdata/apolloservice/output/auth-directive.graphql +++ b/graphql/schema/testdata/apolloservice/output/auth-directive.graphql @@ -198,6 +198,7 @@ directive @secret(field: String!, pred: String) on OBJECT | INTERFACE directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE input IntFilter { eq: Int diff --git a/graphql/schema/testdata/apolloservice/output/custom-directive.graphql b/graphql/schema/testdata/apolloservice/output/custom-directive.graphql index f3365a066ee..cab532ea364 100644 --- a/graphql/schema/testdata/apolloservice/output/custom-directive.graphql +++ b/graphql/schema/testdata/apolloservice/output/custom-directive.graphql @@ -190,6 +190,7 @@ directive @secret(field: String!, pred: String) on OBJECT | INTERFACE directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE input IntFilter { eq: Int diff --git a/graphql/schema/testdata/apolloservice/output/extended-types.graphql b/graphql/schema/testdata/apolloservice/output/extended-types.graphql index ca84d46ff9e..96ce76f371f 100644 --- a/graphql/schema/testdata/apolloservice/output/extended-types.graphql +++ b/graphql/schema/testdata/apolloservice/output/extended-types.graphql @@ -204,6 +204,7 @@ directive @secret(field: String!, pred: String) on OBJECT | INTERFACE directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE input IntFilter { eq: Int diff --git a/graphql/schema/testdata/apolloservice/output/generate-directive.graphql b/graphql/schema/testdata/apolloservice/output/generate-directive.graphql index 6328cb1a3a2..683ce84f0a5 100644 --- a/graphql/schema/testdata/apolloservice/output/generate-directive.graphql +++ b/graphql/schema/testdata/apolloservice/output/generate-directive.graphql @@ -200,6 +200,7 @@ directive @secret(field: String!, pred: String) on OBJECT | INTERFACE directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE input IntFilter { eq: Int diff --git a/graphql/schema/testdata/apolloservice/output/single-extended-type.graphql b/graphql/schema/testdata/apolloservice/output/single-extended-type.graphql index a9621a57690..5d486a6571e 100644 --- a/graphql/schema/testdata/apolloservice/output/single-extended-type.graphql +++ b/graphql/schema/testdata/apolloservice/output/single-extended-type.graphql @@ -185,6 +185,7 @@ directive @secret(field: String!, pred: String) on OBJECT | INTERFACE directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE input IntFilter { eq: Int diff --git a/graphql/schema/testdata/schemagen/output/apollo-federation.graphql b/graphql/schema/testdata/schemagen/output/apollo-federation.graphql index 9594dae7e42..fe85b12a1bd 100644 --- a/graphql/schema/testdata/schemagen/output/apollo-federation.graphql +++ b/graphql/schema/testdata/schemagen/output/apollo-federation.graphql @@ -228,6 +228,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/auth-on-interfaces.graphql b/graphql/schema/testdata/schemagen/output/auth-on-interfaces.graphql index 0c32506a0bf..015dc6c2a6f 100644 --- a/graphql/schema/testdata/schemagen/output/auth-on-interfaces.graphql +++ b/graphql/schema/testdata/schemagen/output/auth-on-interfaces.graphql @@ -210,6 +210,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/authorization.graphql b/graphql/schema/testdata/schemagen/output/authorization.graphql index 81ff4642231..04e0c7f4e4c 100644 --- a/graphql/schema/testdata/schemagen/output/authorization.graphql +++ b/graphql/schema/testdata/schemagen/output/authorization.graphql @@ -206,6 +206,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql b/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql index 7d62c7f57d6..a45e8241b70 100755 --- a/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql +++ b/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql @@ -219,6 +219,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/custom-dql-query-with-subscription.graphql b/graphql/schema/testdata/schemagen/output/custom-dql-query-with-subscription.graphql index 8021d066bba..c58d9b55ed2 100755 --- a/graphql/schema/testdata/schemagen/output/custom-dql-query-with-subscription.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-dql-query-with-subscription.graphql @@ -207,6 +207,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/custom-mutation.graphql b/graphql/schema/testdata/schemagen/output/custom-mutation.graphql index 9e83ca2cda3..5239f106af9 100644 --- a/graphql/schema/testdata/schemagen/output/custom-mutation.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-mutation.graphql @@ -197,6 +197,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql b/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql index a3600db4919..339c3141d9a 100755 --- a/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql @@ -214,6 +214,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 fd9488dffb4..ba54bc9edb0 100644 --- a/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql @@ -198,6 +198,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 94f729622c6..e8544d02bd1 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 @@ -197,6 +197,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 ddd4aa3d108..c70eba52a85 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 @@ -193,6 +193,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/deprecated.graphql b/graphql/schema/testdata/schemagen/output/deprecated.graphql index f8c26352ffc..965cfb97e75 100755 --- a/graphql/schema/testdata/schemagen/output/deprecated.graphql +++ b/graphql/schema/testdata/schemagen/output/deprecated.graphql @@ -193,6 +193,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 c0b66f0fee2..a9acb1557db 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 @@ -210,6 +210,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 80ed446f569..7ab7e16828c 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 @@ -210,6 +210,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 953cd61e262..b223c5e7264 100755 --- a/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql @@ -207,6 +207,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/field-with-multiple-@id-fields.graphql b/graphql/schema/testdata/schemagen/output/field-with-multiple-@id-fields.graphql index 4b31be404c7..5d669349ffc 100755 --- a/graphql/schema/testdata/schemagen/output/field-with-multiple-@id-fields.graphql +++ b/graphql/schema/testdata/schemagen/output/field-with-multiple-@id-fields.graphql @@ -207,6 +207,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 9183dbef464..af4254aa42a 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 @@ -202,6 +202,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql index d3c5022dd68..aa1c90632ac 100644 --- a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql @@ -205,6 +205,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql index 59bd511ecaf..0792f884486 100644 --- a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql @@ -209,6 +209,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql index 520ba3e5328..b10687cd7b0 100644 --- a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql @@ -197,6 +197,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql index de256339647..4a090608150 100644 --- a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql @@ -207,6 +207,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/generate-directive.graphql b/graphql/schema/testdata/schemagen/output/generate-directive.graphql index 6404e68322a..235c68bd89a 100644 --- a/graphql/schema/testdata/schemagen/output/generate-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/generate-directive.graphql @@ -208,6 +208,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/geo-type.graphql b/graphql/schema/testdata/schemagen/output/geo-type.graphql index a89e5a292f8..0df49f78dad 100644 --- a/graphql/schema/testdata/schemagen/output/geo-type.graphql +++ b/graphql/schema/testdata/schemagen/output/geo-type.graphql @@ -199,6 +199,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 3e91c1c404c..109fdf3591b 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 @@ -218,6 +218,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql index 0f7075e7ab7..5228e55ba91 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql @@ -220,6 +220,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 3e91c1c404c..109fdf3591b 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 @@ -218,6 +218,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/hasInverse.graphql b/graphql/schema/testdata/schemagen/output/hasInverse.graphql index 8d8fda76f3c..8a06f7223d5 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse.graphql @@ -199,6 +199,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql b/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql index 4570c63a154..09c31036f63 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql @@ -199,6 +199,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/hasfilter.graphql b/graphql/schema/testdata/schemagen/output/hasfilter.graphql index b47d87e0adb..f1305f00787 100644 --- a/graphql/schema/testdata/schemagen/output/hasfilter.graphql +++ b/graphql/schema/testdata/schemagen/output/hasfilter.graphql @@ -201,6 +201,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql b/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql index fca7182b545..972a73bb14c 100755 --- a/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql @@ -200,6 +200,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 e2e91f6656b..19072f3acb4 100644 --- a/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql @@ -209,6 +209,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/interface-with-id-directive-and-ID-field.graphql b/graphql/schema/testdata/schemagen/output/interface-with-id-directive-and-ID-field.graphql index 2d5491a6427..9f554082682 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-id-directive-and-ID-field.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-id-directive-and-ID-field.graphql @@ -194,6 +194,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 86615951d75..8798ee6f609 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql @@ -203,6 +203,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 b2533501b46..d1f5a3e2df0 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql @@ -203,6 +203,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 3983f0eb1df..3f890b4592b 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 @@ -228,6 +228,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql b/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql index 6ee5da95d2e..32d6abb1b25 100755 --- a/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql +++ b/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql @@ -228,6 +228,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/lambda-directive.graphql b/graphql/schema/testdata/schemagen/output/lambda-directive.graphql index 0a0c20ad5b3..aae76e26023 100644 --- a/graphql/schema/testdata/schemagen/output/lambda-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/lambda-directive.graphql @@ -195,6 +195,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 c9fa50b1c22..9cfdeba87ca 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 @@ -192,6 +192,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/no-id-field.graphql b/graphql/schema/testdata/schemagen/output/no-id-field.graphql index b37bba5bf1b..e4aa4d7fc80 100755 --- a/graphql/schema/testdata/schemagen/output/no-id-field.graphql +++ b/graphql/schema/testdata/schemagen/output/no-id-field.graphql @@ -205,6 +205,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/password-type.graphql b/graphql/schema/testdata/schemagen/output/password-type.graphql index be49ea50c2b..368b2c325c4 100755 --- a/graphql/schema/testdata/schemagen/output/password-type.graphql +++ b/graphql/schema/testdata/schemagen/output/password-type.graphql @@ -193,6 +193,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/random.graphql b/graphql/schema/testdata/schemagen/output/random.graphql index c1d9430d0c3..7f3ae6caae3 100644 --- a/graphql/schema/testdata/schemagen/output/random.graphql +++ b/graphql/schema/testdata/schemagen/output/random.graphql @@ -203,6 +203,7 @@ directive @secret(field: String!, pred: String) on OBJECT | INTERFACE directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY input IntFilter { diff --git a/graphql/schema/testdata/schemagen/output/searchables-references.graphql b/graphql/schema/testdata/schemagen/output/searchables-references.graphql index 689fe3dbed5..f2b10abad28 100755 --- a/graphql/schema/testdata/schemagen/output/searchables-references.graphql +++ b/graphql/schema/testdata/schemagen/output/searchables-references.graphql @@ -203,6 +203,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/searchables.graphql b/graphql/schema/testdata/schemagen/output/searchables.graphql index 3167ec48ef2..331e38376f9 100755 --- a/graphql/schema/testdata/schemagen/output/searchables.graphql +++ b/graphql/schema/testdata/schemagen/output/searchables.graphql @@ -223,6 +223,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 6fdadc828d0..20aceec417f 100755 --- a/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql +++ b/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql @@ -201,6 +201,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/single-type.graphql b/graphql/schema/testdata/schemagen/output/single-type.graphql index 7ab544b96ed..d398335ba8d 100755 --- a/graphql/schema/testdata/schemagen/output/single-type.graphql +++ b/graphql/schema/testdata/schemagen/output/single-type.graphql @@ -196,6 +196,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 0452656682d..33b1dd36f9c 100755 --- a/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql +++ b/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql @@ -210,6 +210,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/type-reference.graphql b/graphql/schema/testdata/schemagen/output/type-reference.graphql index 8b6b4ac64bb..faab9408bfd 100755 --- a/graphql/schema/testdata/schemagen/output/type-reference.graphql +++ b/graphql/schema/testdata/schemagen/output/type-reference.graphql @@ -200,6 +200,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 0168d7d08fd..c2cecc05787 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 @@ -201,6 +201,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 42f7f42e715..cde1c6483aa 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 @@ -200,6 +200,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, 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 93bc3bb78d6..90d9c5e8d60 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 @@ -200,6 +200,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql b/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql index 18e2bdc56e1..f223499c0c2 100644 --- a/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql +++ b/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql @@ -195,6 +195,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/testdata/schemagen/output/union.graphql b/graphql/schema/testdata/schemagen/output/union.graphql index 22b13dc669e..dc726070c0c 100644 --- a/graphql/schema/testdata/schemagen/output/union.graphql +++ b/graphql/schema/testdata/schemagen/output/union.graphql @@ -242,6 +242,7 @@ directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @remoteResponse(name: String) on FIELD_DEFINITION directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION +directive @lambdaOnMutate(add: Boolean, update: Boolean, delete: Boolean) on OBJECT | INTERFACE directive @cacheControl(maxAge: Int!) on QUERY directive @generate( query: GenerateQueryParams, diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index 3ab87ab1ef1..edb0a2d71c8 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -211,6 +211,7 @@ type Mutation interface { MutatedType() Type QueryField() Field NumUidsField() Field + HasLambdaOnMutate() bool } // A Query is a field (from the schema's Query type) from an Operation @@ -305,6 +306,10 @@ type schema struct { // lambdaDirectives stores the mapping of typeName->fieldName->true, if the field has @lambda. // It is read-only. lambdaDirectives map[string]map[string]bool + // lambdaOnMutate stores the mapping of mutationName -> true, if the config of @lambdaOnMutate + // enables lambdas for that mutation. + // It is read-only. + lambdaOnMutate map[string]bool // requiresDirectives stores the mapping of typeName->fieldName->list of fields given in // @requires. It is read-only. requiresDirectives map[string]map[string][]string @@ -687,7 +692,7 @@ func typeMappings(s *ast.Schema) map[string][]*ast.Definition { // }) // 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, +func customAndLambdaMappings(s *ast.Schema, ns uint64) (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) @@ -724,7 +729,7 @@ func customAndLambdaMappings(s *ast.Schema) (map[string]map[string]*ast.Directiv // 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 { + dir, ns, 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 @@ -827,8 +832,8 @@ func (m *mutation) IsExternal() bool { return (*field)(m).IsExternal() } -func (f *fieldDefinition) IsExternal() bool { - return hasExternal(f.fieldDef) +func (fd *fieldDefinition) IsExternal() bool { + return hasExternal(fd.fieldDef) } func hasCustomOrLambda(f *ast.FieldDefinition) bool { @@ -877,7 +882,8 @@ func externalAndNonKeyField(fld *ast.FieldDefinition, defn *ast.Definition, prov // 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 { + lambdaDir *ast.Directive, ns uint64, skipInBodyTemplate func(f *ast.FieldDefinition) bool) *ast. + Directive { comma := "" var bodyTemplate strings.Builder @@ -912,7 +918,7 @@ func buildCustomDirectiveForLambda(defn *ast.Definition, field *ast.FieldDefinit // build the children for http argument httpArgChildrens := []*ast.ChildValue{ - getChildValue(httpUrl, x.Config.GraphQL.GetString("lambda-url"), ast.StringValue, lambdaDir.Position), + getChildValue(httpUrl, x.LambdaUrl(ns), ast.StringValue, lambdaDir.Position), getChildValue(httpMethod, http.MethodPost, ast.EnumValue, lambdaDir.Position), getChildValue(httpBody, bodyTemplate.String(), ast.StringValue, lambdaDir.Position), } @@ -945,10 +951,27 @@ func getChildValue(name, raw string, kind ast.ValueKind, position *ast.Position) } } +func lambdaOnMutateMappings(s *ast.Schema) map[string]bool { + result := make(map[string]bool) + for _, typ := range s.Types { + dir := typ.Directives.ForName(lambdaOnMutateDirective) + if dir == nil { + continue + } + + for _, arg := range dir.Arguments { + value, _ := arg.Value.Value(nil) + if val, ok := value.(bool); ok && val { + result[arg.Name+typ.Name] = true + } + } + } + return result +} + // AsSchema wraps a github.com/dgraph-io/gqlparser/ast.Schema. -func AsSchema(s *ast.Schema) (Schema, error) { - customDirs, lambdaDirs := customAndLambdaMappings(s) - remoteResponseDirs := remoteResponseMapping(s) +func AsSchema(s *ast.Schema, ns uint64) (Schema, error) { + customDirs, lambdaDirs := customAndLambdaMappings(s, ns) dgraphPredicate := dgraphMapping(s) sch := &schema{ schema: s, @@ -956,8 +979,9 @@ func AsSchema(s *ast.Schema) (Schema, error) { typeNameAst: typeMappings(s), customDirectives: customDirs, lambdaDirectives: lambdaDirs, + lambdaOnMutate: lambdaOnMutateMappings(s), requiresDirectives: requiresMappings(s), - remoteResponse: remoteResponseDirs, + remoteResponse: remoteResponseMapping(s), meta: &metaInfo{}, // initialize with an empty metaInfo } sch.mutatedType = mutatedTypeMapping(sch, dgraphPredicate) @@ -2138,6 +2162,10 @@ func (m *mutation) NumUidsField() Field { return nil } +func (m *mutation) HasLambdaOnMutate() bool { + return m.op.inSchema.lambdaOnMutate[m.Name()] +} + func (m *mutation) Location() x.Location { return (*field)(m).Location() } diff --git a/graphql/schema/wrappers_test.go b/graphql/schema/wrappers_test.go index 35b20e310eb..0d2518f573f 100644 --- a/graphql/schema/wrappers_test.go +++ b/graphql/schema/wrappers_test.go @@ -87,7 +87,7 @@ type Starship { schHandler, errs := NewHandler(schemaStr, false) require.NoError(t, errs) - sch, err := FromString(schHandler.GQLSchema()) + sch, err := FromString(schHandler.GQLSchema(), x.GalaxyNamespace) require.NoError(t, err) s, ok := sch.(*schema) @@ -272,7 +272,7 @@ func TestDgraphMapping_WithDirectives(t *testing.T) { schHandler, errs := NewHandler(schemaStr, false) require.NoError(t, errs) - sch, err := FromString(schHandler.GQLSchema()) + sch, err := FromString(schHandler.GQLSchema(), x.GalaxyNamespace) require.NoError(t, err) s, ok := sch.(*schema) @@ -408,7 +408,7 @@ func TestCheckNonNulls(t *testing.T) { req: String! notReq: String alsoReq: String! - }`) + }`, x.GalaxyNamespace) require.NoError(t, err) tcases := map[string]struct { @@ -918,7 +918,7 @@ func TestGraphQLQueryInCustomHTTPConfig(t *testing.T) { t.Run(tcase.Name, func(t *testing.T) { schHandler, errs := NewHandler(tcase.GQLSchema, false) require.NoError(t, errs) - sch, err := FromString(schHandler.GQLSchema()) + sch, err := FromString(schHandler.GQLSchema(), x.GalaxyNamespace) require.NoError(t, err) var vars map[string]interface{} @@ -958,7 +958,7 @@ func TestGraphQLQueryInCustomHTTPConfig(t *testing.T) { remoteSchemaHandler, errs := NewHandler(tcase.RemoteSchema, false) require.NoError(t, errs) - remoteSchema, err := FromString(remoteSchemaHandler.GQLSchema()) + remoteSchema, err := FromString(remoteSchemaHandler.GQLSchema(), x.GalaxyNamespace) require.NoError(t, err) // Validate the generated query against the remote schema. @@ -1018,7 +1018,7 @@ func TestAllowedHeadersList(t *testing.T) { t.Run(test.name, func(t *testing.T) { schHandler, errs := NewHandler(test.schemaStr, false) require.NoError(t, errs) - _, err := FromString(schHandler.GQLSchema()) + _, err := FromString(schHandler.GQLSchema(), x.GalaxyNamespace) require.NoError(t, err) require.Equal(t, strings.Join([]string{x.AccessControlAllowedHeaders, test.expected}, ","), schHandler.MetaInfo().AllowedCorsHeaders()) diff --git a/graphql/test/test.go b/graphql/test/test.go index 4daf53de754..faf0a662f64 100644 --- a/graphql/test/test.go +++ b/graphql/test/test.go @@ -21,6 +21,8 @@ import ( "io/ioutil" "testing" + "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/dgraph/graphql/schema" "github.com/dgraph-io/gqlparser/v2/ast" "github.com/dgraph-io/gqlparser/v2/parser" @@ -40,7 +42,7 @@ func LoadSchema(t *testing.T, gqlSchema string) schema.Schema { gql, gqlErr := validator.ValidateSchemaDocument(doc) requireNoGQLErrors(t, gqlErr) - schema, err := schema.AsSchema(gql) + schema, err := schema.AsSchema(gql, x.GalaxyNamespace) requireNoGQLErrors(t, err) return schema } diff --git a/x/config.go b/x/config.go index f5447f5cf0a..d71dfe6e2b6 100644 --- a/x/config.go +++ b/x/config.go @@ -46,6 +46,15 @@ type Options struct { // extensions bool - Will be set to see extensions in GraphQL results // debug bool - Will enable debug mode in GraphQL. // lambda-url string - Stores the URL of lambda functions for custom GraphQL resolvers + // The configured lambda-url can have a parameter `$ns`, + // which should be replaced with the correct namespace value at runtime. + // =========================================================================================== + // | lambda-url | $ns | namespacedLambdaUrl | + // |==========================================|=====|========================================| + // | http://localhost:8686/graphql-worker/$ns | 1 | http://localhost:8686/graphql-worker/1 | + // | http://localhost:8686/graphql-worker | 1 | http://localhost:8686/graphql-worker | + // |=========================================================================================| + // // poll-interval duration - The polling interval for graphql subscription. GraphQL *z.SuperFlag GraphQLDebug bool diff --git a/x/jwt_helper.go b/x/jwt_helper.go index 10c9a784673..a51e3798a96 100644 --- a/x/jwt_helper.go +++ b/x/jwt_helper.go @@ -61,7 +61,7 @@ func ExtractJWTNamespace(ctx context.Context) (uint64, error) { if err != nil { return 0, err } - claims, err := ParseJWT(jwtString[0]) + claims, err := ParseJWT(jwtString) if err != nil { return 0, err } diff --git a/x/x.go b/x/x.go index 8927beb9571..c787ad17849 100644 --- a/x/x.go +++ b/x/x.go @@ -301,18 +301,18 @@ func GetForceNamespace(ctx context.Context) string { return ns[0] } -func ExtractJwt(ctx context.Context) ([]string, error) { +func ExtractJwt(ctx context.Context) (string, error) { // extract the jwt and unmarshal the jwt to get the list of groups md, ok := metadata.FromIncomingContext(ctx) if !ok { - return nil, ErrNoJwt + return "", ErrNoJwt } accessJwt := md.Get("accessJwt") if len(accessJwt) == 0 { - return nil, ErrNoJwt + return "", ErrNoJwt } - return accessJwt, nil + return accessJwt[0], nil } // WithLocations adds a list of locations to a GqlError and returns the same @@ -1382,3 +1382,9 @@ func PrefixesToMatches(prefixes [][]byte, ignore string) []*pb.Match { } return matches } + +// LambdaUrl returns the correct lambda-url for the given namespace +func LambdaUrl(ns uint64) string { + return strings.Replace(Config.GraphQL.GetString("lambda-url"), "$ns", strconv.FormatUint(ns, + 10), 1) +}