From d9e95a923e6a314c30c69d44d8f54130e6e7f997 Mon Sep 17 00:00:00 2001 From: Balaji Jinnah Date: Tue, 8 Sep 2020 14:16:58 +0530 Subject: [PATCH] feat: add schema history to graphql (#6324) --- dgraph/cmd/bulk/systest/test-bulk-schema.sh | 2 + edgraph/graphql.go | 153 ++++++++++++++++++++ edgraph/server.go | 127 ---------------- ee/acl/acl_test.go | 28 +++- graphql/admin/admin.go | 56 +++++-- graphql/admin/schema.go | 17 ++- graphql/e2e/common/admin.go | 72 +++++++++ graphql/e2e/directives/schema_response.json | 18 +++ graphql/e2e/normal/schema_response.json | 18 +++ graphql/e2e/schema/schema_test.go | 121 +++++++++++++++- graphql/resolve/resolver.go | 1 - schema/schema.go | 17 +++ systest/backup/encryption/backup_test.go | 5 +- systest/backup/filesystem/backup_test.go | 5 +- systest/backup/minio/backup_test.go | 5 +- systest/export/export_test.go | 6 + systest/queries_test.go | 6 + worker/export.go | 5 +- x/keys.go | 17 ++- x/x.go | 5 + 20 files changed, 523 insertions(+), 161 deletions(-) create mode 100644 edgraph/graphql.go diff --git a/dgraph/cmd/bulk/systest/test-bulk-schema.sh b/dgraph/cmd/bulk/systest/test-bulk-schema.sh index d650c7426d4..e2b15ea9b50 100755 --- a/dgraph/cmd/bulk/systest/test-bulk-schema.sh +++ b/dgraph/cmd/bulk/systest/test-bulk-schema.sh @@ -202,6 +202,8 @@ EOF 1 dgraph.acl.rule 1 dgraph.cors 1 dgraph.graphql.schema + 1 dgraph.graphql.schema_created_at + 1 dgraph.graphql.schema_history 1 dgraph.graphql.xid 1 dgraph.password 1 dgraph.rule.permission diff --git a/edgraph/graphql.go b/edgraph/graphql.go new file mode 100644 index 00000000000..3368a53b542 --- /dev/null +++ b/edgraph/graphql.go @@ -0,0 +1,153 @@ +/* + * Copyright 2017-2020 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package edgraph + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "time" + + "github.com/dgraph-io/dgo/v200/protos/api" + "github.com/dgraph-io/ristretto/z" + "github.com/golang/glog" +) + +// ResetCors make the dgraph to accept all the origins if no origins were given +// by the users. +func ResetCors(closer *z.Closer) { + defer func() { + glog.Infof("ResetCors closed") + closer.Done() + }() + + req := &api.Request{ + Query: `query{ + cors as var(func: has(dgraph.cors)) + }`, + Mutations: []*api.Mutation{ + { + Set: []*api.NQuad{ + { + Subject: "_:a", + Predicate: "dgraph.cors", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "*"}}, + }, + }, + Cond: `@if(eq(len(cors), 0))`, + }, + }, + CommitNow: true, + } + + for closer.Ctx().Err() == nil { + ctx, cancel := context.WithTimeout(closer.Ctx(), time.Minute) + defer cancel() + ctx = context.WithValue(ctx, IsGraphql, true) + if _, err := (&Server{}).doQuery(ctx, req, NoAuthorize); err != nil { + glog.Infof("Unable to upsert cors. Error: %v", err) + time.Sleep(100 * time.Millisecond) + } + break + } +} + +func generateNquadsForCors(origins []string) []byte { + out := &bytes.Buffer{} + for _, origin := range origins { + out.Write([]byte(fmt.Sprintf("uid(cors) \"%s\" . \n", origin))) + } + return out.Bytes() +} + +// AddCorsOrigins Adds the cors origins to the Dgraph. +func AddCorsOrigins(ctx context.Context, origins []string) error { + req := &api.Request{ + Query: `query{ + cors as var(func: has(dgraph.cors)) + }`, + Mutations: []*api.Mutation{ + { + SetNquads: generateNquadsForCors(origins), + Cond: `@if(eq(len(cors), 1))`, + DelNquads: []byte(`uid(cors) * .`), + }, + }, + CommitNow: true, + } + _, err := (&Server{}).doQuery(context.WithValue(ctx, IsGraphql, true), req, NoAuthorize) + return err +} + +// GetCorsOrigins retrive all the cors origin from the database. +func GetCorsOrigins(ctx context.Context) ([]string, error) { + req := &api.Request{ + Query: `query{ + me(func: has(dgraph.cors)){ + dgraph.cors + } + }`, + ReadOnly: true, + } + res, err := (&Server{}).doQuery(context.WithValue(ctx, IsGraphql, true), req, NoAuthorize) + if err != nil { + return nil, err + } + + type corsResponse struct { + Me []struct { + DgraphCors []string `json:"dgraph.cors"` + } `json:"me"` + } + corsRes := &corsResponse{} + if err = json.Unmarshal(res.Json, corsRes); err != nil { + return nil, err + } + if len(corsRes.Me) != 1 { + return []string{}, fmt.Errorf("GetCorsOrigins returned %d results", len(corsRes.Me)) + } + return corsRes.Me[0].DgraphCors, nil +} + +// UpdateSchemaHistory updates graphql schema history. +func UpdateSchemaHistory(ctx context.Context, schema string) error { + req := &api.Request{ + Mutations: []*api.Mutation{ + { + Set: []*api.NQuad{ + { + Subject: "_:a", + Predicate: "dgraph.graphql.schema_history", + ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: schema}}, + }, + { + Subject: "_:a", + Predicate: "dgraph.type", + ObjectValue: &api.Value{Val: &api.Value_StrVal{ + StrVal: "dgraph.graphql.history"}}, + }, + }, + SetNquads: []byte(fmt.Sprintf(`_:a "%s" .`, + time.Now().Format(time.RFC3339))), + }, + }, + CommitNow: true, + } + _, err := (&Server{}).doQuery(context.WithValue(ctx, IsGraphql, true), req, NoAuthorize) + return err +} diff --git a/edgraph/server.go b/edgraph/server.go index 0b97223d1d1..9a72290f20a 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -58,7 +58,6 @@ import ( "github.com/dgraph-io/dgraph/types/facets" "github.com/dgraph-io/dgraph/worker" "github.com/dgraph-io/dgraph/x" - "github.com/dgraph-io/ristretto/z" ) const ( @@ -85,9 +84,6 @@ const ( // NoAuthorize is used to indicate that authorization needs to be skipped. // Used when ACL needs to query information for performing the authorization check. NoAuthorize - // CorsMutationAllowed is used to indicate that the given request is authorized to do - // cors mutation. - CorsMutationAllowed ) var ( @@ -989,11 +985,6 @@ func (s *Server) doQuery(ctx context.Context, req *api.Request, doAuth AuthMode) } } - if doAuth != CorsMutationAllowed { - if rerr = validateCorsInMutation(ctx, qc); rerr != nil { - return - } - } // We use defer here because for queries, startTs will be // assigned in the processQuery function called below. defer annotateStartTs(qc.span, qc.req.StartTs) @@ -1231,29 +1222,6 @@ func authorizeRequest(ctx context.Context, qc *queryContext) error { return nil } -// validateCorsInMutation check whether mutation contains cors predication. If it's contain cors -// predicate, we'll throw an error. -func validateCorsInMutation(ctx context.Context, qc *queryContext) error { - validateNquad := func(nquads []*api.NQuad) error { - for _, nquad := range nquads { - if nquad.Predicate != "dgraph.cors" { - continue - } - return errors.New("Mutations are not allowed for the predicate dgraph.cors") - } - return nil - } - for _, gmu := range qc.gmuList { - if err := validateNquad(gmu.Set); err != nil { - return err - } - if err := validateNquad(gmu.Del); err != nil { - return err - } - } - return nil -} - // CommitOrAbort commits or aborts a transaction. func (s *Server) CommitOrAbort(ctx context.Context, tc *api.TxnContext) (*api.TxnContext, error) { ctx, span := otrace.StartSpan(ctx, "Server.CommitOrAbort") @@ -1548,98 +1516,3 @@ func isDropAll(op *api.Operation) bool { } return false } - -// ResetCors make the dgraph to accept all the origins if no origins were given -// by the users. -func ResetCors(closer *z.Closer) { - defer func() { - glog.Infof("ResetCors closed") - closer.Done() - }() - - req := &api.Request{ - Query: `query{ - cors as var(func: has(dgraph.cors)) - }`, - Mutations: []*api.Mutation{ - { - Set: []*api.NQuad{ - { - Subject: "_:a", - Predicate: "dgraph.cors", - ObjectValue: &api.Value{Val: &api.Value_StrVal{StrVal: "*"}}, - }, - }, - Cond: `@if(eq(len(cors), 0))`, - }, - }, - CommitNow: true, - } - - for closer.Ctx().Err() == nil { - ctx, cancel := context.WithTimeout(closer.Ctx(), time.Minute) - defer cancel() - if _, err := (&Server{}).doQuery(ctx, req, CorsMutationAllowed); err != nil { - glog.Infof("Unable to upsert cors. Error: %v", err) - time.Sleep(100 * time.Millisecond) - } - break - } -} - -func generateNquadsForCors(origins []string) []byte { - out := &bytes.Buffer{} - for _, origin := range origins { - out.Write([]byte(fmt.Sprintf("uid(cors) \"%s\" . \n", origin))) - } - return out.Bytes() -} - -// AddCorsOrigins Adds the cors origins to the Dgraph. -func AddCorsOrigins(ctx context.Context, origins []string) error { - req := &api.Request{ - Query: `query{ - cors as var(func: has(dgraph.cors)) - }`, - Mutations: []*api.Mutation{ - { - SetNquads: generateNquadsForCors(origins), - Cond: `@if(eq(len(cors), 1))`, - DelNquads: []byte(`uid(cors) * .`), - }, - }, - CommitNow: true, - } - _, err := (&Server{}).doQuery(ctx, req, CorsMutationAllowed) - return err -} - -// GetCorsOrigins retrive all the cors origin from the database. -func GetCorsOrigins(ctx context.Context) ([]string, error) { - req := &api.Request{ - Query: `query{ - me(func: has(dgraph.cors)){ - dgraph.cors - } - }`, - ReadOnly: true, - } - res, err := (&Server{}).doQuery(ctx, req, NoAuthorize) - if err != nil { - return nil, err - } - - type corsResponse struct { - Me []struct { - DgraphCors []string `json:"dgraph.cors"` - } `json:"me"` - } - corsRes := &corsResponse{} - if err = json.Unmarshal(res.Json, corsRes); err != nil { - return nil, err - } - if len(corsRes.Me) != 1 { - return []string{}, fmt.Errorf("GetCorsOrigins returned %d results", len(corsRes.Me)) - } - return corsRes.Me[0].DgraphCors, nil -} diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index a3de9e6f410..c5c31f31d82 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -2093,7 +2093,15 @@ func TestSchemaQueryWithACL(t *testing.T) { { "predicate": "dgraph.graphql.schema", "type": "string" - }, + }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", + "type": "string" + }, { "predicate": "dgraph.graphql.xid", "type": "string", @@ -2156,7 +2164,17 @@ func TestSchemaQueryWithACL(t *testing.T) { } ], "name": "dgraph.graphql" - }, + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" + },{ + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" + }, { "fields": [ { @@ -2210,7 +2228,11 @@ func TestSchemaQueryWithACL(t *testing.T) { { "fields": [], "name": "dgraph.graphql" - }, + }, + { + "fields": [], + "name": "dgraph.graphql.history" + }, { "fields": [], "name": "dgraph.type.Group" diff --git a/graphql/admin/admin.go b/graphql/admin/admin.go index 0206133aba1..38f98bbaf85 100644 --- a/graphql/admin/admin.go +++ b/graphql/admin/admin.go @@ -47,6 +47,8 @@ const ( // GraphQL schema for /admin endpoint. graphqlAdminSchema = ` + scalar DateTime + """ Data about the GraphQL schema being served by Dgraph. """ @@ -69,6 +71,14 @@ const ( acceptedOrigins: [String] } + """ + SchemaHistory contains the schema and the time when the schema has been created. + """ + type SchemaHistory @dgraph(type: "dgraph.graphql.history") { + schema: String! @id @dgraph(pred: "dgraph.graphql.schema_history") + created_at: DateTime! @dgraph(pred: "dgraph.graphql.schema_created_at") + } + """ A NodeState is the state of an individual node in the Dgraph cluster. """ @@ -269,7 +279,7 @@ const ( state: MembershipState config: Config getAllowedCORSOrigins: Cors - + querySchemaHistory(first: Int, offset: Int): [SchemaHistory] ` + adminQueries + ` } @@ -332,11 +342,13 @@ var ( "getGQLSchema": commonAdminQueryMWs, // for queries and mutations related to User/Group, dgraph handles Guardian auth, // so no need to apply GuardianAuth Middleware - "queryGroup": {resolve.IpWhitelistingMW4Query}, - "queryUser": {resolve.IpWhitelistingMW4Query}, - "getGroup": {resolve.IpWhitelistingMW4Query}, - "getCurrentUser": {resolve.IpWhitelistingMW4Query}, - "getUser": {resolve.IpWhitelistingMW4Query}, + "queryGroup": {resolve.IpWhitelistingMW4Query}, + "queryUser": {resolve.IpWhitelistingMW4Query}, + "getGroup": {resolve.IpWhitelistingMW4Query}, + "getCurrentUser": {resolve.IpWhitelistingMW4Query}, + "getUser": {resolve.IpWhitelistingMW4Query}, + "querySchemaHistory": {resolve.IpWhitelistingMW4Query}, + "getAllowedCORSOrigins": {resolve.IpWhitelistingMW4Query}, } adminMutationMWConfig = map[string]resolve.MutationMiddlewares{ "backup": commonAdminMutationMWs, @@ -349,12 +361,13 @@ var ( "updateGQLSchema": commonAdminMutationMWs, // for queries and mutations related to User/Group, dgraph handles Guardian auth, // so no need to apply GuardianAuth Middleware - "addUser": {resolve.IpWhitelistingMW4Mutation}, - "addGroup": {resolve.IpWhitelistingMW4Mutation}, - "updateUser": {resolve.IpWhitelistingMW4Mutation}, - "updateGroup": {resolve.IpWhitelistingMW4Mutation}, - "deleteUser": {resolve.IpWhitelistingMW4Mutation}, - "deleteGroup": {resolve.IpWhitelistingMW4Mutation}, + "addUser": {resolve.IpWhitelistingMW4Mutation}, + "addGroup": {resolve.IpWhitelistingMW4Mutation}, + "updateUser": {resolve.IpWhitelistingMW4Mutation}, + "updateGroup": {resolve.IpWhitelistingMW4Mutation}, + "deleteUser": {resolve.IpWhitelistingMW4Mutation}, + "deleteGroup": {resolve.IpWhitelistingMW4Mutation}, + "replaceAllowedCORSOrigins": {resolve.IpWhitelistingMW4Mutation}, } // mainHealthStore stores the health of the main GraphQL server. mainHealthStore = &GraphQLHealthStore{} @@ -588,6 +601,12 @@ func newAdminResolverFactory() resolve.ResolverFactory { func(ctx context.Context, query schema.Query) *resolve.Resolved { return &resolve.Resolved{Err: errors.Errorf(errMsgServerNotReady), Field: q} }) + }). + WithQueryResolver("querySchemaHistory", func(q schema.Query) resolve.QueryResolver { + return resolve.QueryResolverFunc( + func(ctx context.Context, query schema.Query) *resolve.Resolved { + return &resolve.Resolved{Err: errors.Errorf(errMsgServerNotReady), Field: q} + }) }) for gqlMut, resolver := range adminMutationResolvers { // gotta force go to evaluate the right function at each loop iteration @@ -682,7 +701,9 @@ func (as *adminServer) addConnectedAdminResolvers() { as.rf.WithMutationResolver("updateGQLSchema", func(m schema.Mutation) resolve.MutationResolver { - return resolve.MutationResolverFunc(resolveUpdateGQLSchema) + return &updateSchemaResolver{ + admin: as, + } }). WithQueryResolver("getGQLSchema", func(q schema.Query) resolve.QueryResolver { @@ -737,6 +758,15 @@ func (as *adminServer) addConnectedAdminResolvers() { WithQueryResolver("getAllowedCORSOrigins", func(q schema.Query) resolve.QueryResolver { return resolve.QueryResolverFunc(resolveGetCors) }). + WithQueryResolver("querySchemaHistory", func(q schema.Query) resolve.QueryResolver { + // Add the desceding order to the created_at to get the schema history in + // descending order. + q.Arguments()["order"] = map[string]interface{}{"desc": "created_at"} + return resolve.NewQueryResolver( + qryRw, + dgEx, + resolve.StdQueryCompletion()) + }). WithMutationResolver("addUser", func(m schema.Mutation) resolve.MutationResolver { return resolve.NewDgraphResolver( diff --git a/graphql/admin/schema.go b/graphql/admin/schema.go index b38d7da897d..0ebf5192d75 100644 --- a/graphql/admin/schema.go +++ b/graphql/admin/schema.go @@ -28,6 +28,7 @@ import ( "github.com/dgraph-io/dgraph/graphql/schema" "github.com/dgraph-io/dgraph/query" "github.com/dgraph-io/dgraph/x" + "github.com/dgryski/go-farm" "github.com/golang/glog" ) @@ -41,7 +42,11 @@ type updateGQLSchemaInput struct { Set gqlSchema `json:"set,omitempty"` } -func resolveUpdateGQLSchema(ctx context.Context, m schema.Mutation) (*resolve.Resolved, bool) { +type updateSchemaResolver struct { + admin *adminServer +} + +func (usr *updateSchemaResolver) Resolve(ctx context.Context, m schema.Mutation) (*resolve.Resolved, bool) { glog.Info("Got updateGQLSchema request") input, err := getSchemaInput(m) @@ -60,11 +65,21 @@ func resolveUpdateGQLSchema(ctx context.Context, m schema.Mutation) (*resolve.Re return resolve.EmptyResult(m, err), false } + oldSchemaHash := farm.Fingerprint64([]byte(usr.admin.schema.Schema)) + newSchemaHash := farm.Fingerprint64([]byte(input.Set.Schema)) + updateHistory := oldSchemaHash != newSchemaHash + resp, err := edgraph.UpdateGQLSchema(ctx, input.Set.Schema, schHandler.DGSchema()) if err != nil { return resolve.EmptyResult(m, err), false } + if updateHistory { + if err := edgraph.UpdateSchemaHistory(ctx, input.Set.Schema); err != nil { + glog.Errorf("error while updating schema history %s", err.Error()) + } + } + return &resolve.Resolved{ Data: map[string]interface{}{ m.Name(): map[string]interface{}{ diff --git a/graphql/e2e/common/admin.go b/graphql/e2e/common/admin.go index 063488d3a9d..b1e41305e39 100644 --- a/graphql/e2e/common/admin.go +++ b/graphql/e2e/common/admin.go @@ -57,6 +57,14 @@ const ( "predicate": "dgraph.graphql.schema", "type": "string" }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", + "type": "string" + }, { "predicate": "dgraph.graphql.xid", "type": "string", @@ -86,6 +94,16 @@ const ( } ], "name": "dgraph.graphql" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" + },{ + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" } ] }` @@ -114,6 +132,14 @@ const ( "predicate": "dgraph.graphql.schema", "type": "string" }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", + "type": "string" + }, { "predicate": "dgraph.graphql.xid", "type": "string", @@ -151,6 +177,16 @@ const ( } ], "name": "dgraph.graphql" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" + },{ + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" } ] }` @@ -194,6 +230,14 @@ const ( "predicate": "dgraph.graphql.schema", "type": "string" }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", + "type": "string" + }, { "predicate": "dgraph.graphql.xid", "type": "string", @@ -234,6 +278,16 @@ const ( } ], "name": "dgraph.graphql" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" + },{ + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" } ] }` @@ -285,6 +339,14 @@ const ( "predicate": "dgraph.graphql.schema", "type": "string" }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", + "type": "string" + }, { "predicate": "dgraph.graphql.xid", "type": "string", @@ -328,6 +390,16 @@ const ( } ], "name": "dgraph.graphql" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" + },{ + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" } ] }` diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index ab1dc660641..79ce9f18785 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -171,6 +171,14 @@ "predicate": "dgraph.graphql.schema", "type": "string" }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", + "type": "string" + }, { "predicate": "dgraph.graphql.xid", "type": "string", @@ -577,6 +585,16 @@ ], "name": "dgraph.graphql" }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" + },{ + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" + }, { "fields": [ { diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index aae7b9d46cb..853a1dcd668 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -335,6 +335,14 @@ "predicate": "dgraph.graphql.schema", "type": "string" }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", + "type": "string" + }, { "predicate": "dgraph.graphql.xid", "type": "string", @@ -675,6 +683,16 @@ } ], "name": "dgraph.graphql" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" + },{ + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" } ] } \ No newline at end of file diff --git a/graphql/e2e/schema/schema_test.go b/graphql/e2e/schema/schema_test.go index a818058582f..452801f2940 100644 --- a/graphql/e2e/schema/schema_test.go +++ b/graphql/e2e/schema/schema_test.go @@ -218,9 +218,17 @@ func TestConcurrentSchemaUpdates(t *testing.T) { "exact" ], "upsert": true - }, - { + }, + { "predicate": "dgraph.graphql.schema", + "type": "string" + }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", "type": "string" }, { @@ -260,6 +268,16 @@ func TestConcurrentSchemaUpdates(t *testing.T) { } ], "name": "dgraph.graphql" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" + },{ + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" } ] }`, tcases[lastSuccessTcaseIdx].dgraphSchema) @@ -369,6 +387,105 @@ func TestGQLSchemaAfterDropData(t *testing.T) { } +// TestSchemaHistory checks the admin schema history API working properly or not. +func TestSchemaHistory(t *testing.T) { + // Drop all to remove all the previous schema history. + dg, err := testutil.DgraphClient(groupOnegRPC) + require.NoError(t, err) + require.NoError(t, dg.Alter(context.Background(), &api.Operation{DropOp: api.Operation_DATA, RunInBackground: false})) + + // Let's get the schema. It should return empty results. + get := &common.GraphQLParams{ + Query: `query{ + querySchemaHistory(first:10){ + schema + created_at + } + }`, + } + getResult := get.ExecuteAsPost(t, groupOneAdminServer) + require.Nil(t, getResult.Errors) + + require.JSONEq(t, `{ + "querySchemaHistory": [] + }`, string(getResult.Data)) + + // Let's add an schema and expect the history in the history api. + updateGQLSchemaRequireNoErrors(t, ` + type A { + b: String! + }`, groupOneAdminServer) + time.Sleep(time.Second) + + getResult = get.ExecuteAsPost(t, groupOneAdminServer) + require.Nil(t, getResult.Errors) + type History struct { + Schema string `json:"schema"` + CreatedAt string `json:"created_at"` + } + type schemaHistory struct { + QuerySchemaHistory []History `json:"querySchemaHistory"` + } + history := schemaHistory{} + require.NoError(t, json.Unmarshal(getResult.Data, &history)) + require.Equal(t, int(1), len(history.QuerySchemaHistory)) + require.Equal(t, history.QuerySchemaHistory[0].Schema, "\n\ttype A {\n\t\tb: String!\n\t}") + + // Let's update the same schema. But we should not get the 2 history because, we + // are updating the same schema. + updateGQLSchemaRequireNoErrors(t, ` + type A { + b: String! + }`, groupOneAdminServer) + time.Sleep(time.Second) + + getResult = get.ExecuteAsPost(t, groupOneAdminServer) + require.Nil(t, getResult.Errors) + history = schemaHistory{} + require.NoError(t, json.Unmarshal(getResult.Data, &history)) + require.Equal(t, int(1), len(history.QuerySchemaHistory)) + require.Equal(t, history.QuerySchemaHistory[0].Schema, "\n\ttype A {\n\t\tb: String!\n\t}") + + // Let's update a new schema and check the history. + updateGQLSchemaRequireNoErrors(t, ` + type B { + b: String! + }`, groupOneAdminServer) + time.Sleep(time.Second) + + getResult = get.ExecuteAsPost(t, groupOneAdminServer) + require.Nil(t, getResult.Errors) + history = schemaHistory{} + require.NoError(t, json.Unmarshal(getResult.Data, &history)) + require.Equal(t, int(2), len(history.QuerySchemaHistory)) + require.Equal(t, history.QuerySchemaHistory[0].Schema, "\n\ttype B {\n\t\tb: String!\n\t}") + require.Equal(t, history.QuerySchemaHistory[1].Schema, "\n\ttype A {\n\t\tb: String!\n\t}") + + // Check offset working properly or not. + get = &common.GraphQLParams{ + Query: `query{ + querySchemaHistory(first:10, offset:1){ + schema + created_at + } + }`, + } + getResult = get.ExecuteAsPost(t, groupOneAdminServer) + require.Nil(t, getResult.Errors) + history = schemaHistory{} + require.NoError(t, json.Unmarshal(getResult.Data, &history)) + require.Equal(t, int(1), len(history.QuerySchemaHistory)) + require.Equal(t, history.QuerySchemaHistory[0].Schema, "\n\ttype A {\n\t\tb: String!\n\t}") + + // Let's drop eveything and see whether we getting empty results are not. + require.NoError(t, dg.Alter(context.Background(), &api.Operation{DropOp: api.Operation_DATA, RunInBackground: false})) + getResult = get.ExecuteAsPost(t, groupOneAdminServer) + require.Nil(t, getResult.Errors) + require.JSONEq(t, `{ + "querySchemaHistory": [] + }`, string(getResult.Data)) +} + // verifyEmptySchema verifies that the schema is not set in the GraphQL server. func verifyEmptySchema(t *testing.T) { schema := getGQLSchema(t, groupOneAdminServer) diff --git a/graphql/resolve/resolver.go b/graphql/resolve/resolver.go index 3a3d561e776..b04c4bb6e1e 100644 --- a/graphql/resolve/resolver.go +++ b/graphql/resolve/resolver.go @@ -452,7 +452,6 @@ func (r *RequestResolver) Resolve(ctx context.Context, gqlReq *schema.Request) * Err: err, } }) - allResolved[storeAt] = r.resolvers.queryResolverFor(q).Resolve(ctx, q) }(q, i) } diff --git a/schema/schema.go b/schema/schema.go index e7aec74bb1d..62e4480bbdc 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -566,6 +566,17 @@ func initialTypesInternal(all bool) []*pb.TypeUpdate { ValueType: pb.Posting_STRING, }, }, + }, &pb.TypeUpdate{ + TypeName: "dgraph.graphql.history", + Fields: []*pb.SchemaUpdate{ + { + Predicate: "dgraph.graphql.schema_history", + ValueType: pb.Posting_STRING, + }, { + Predicate: "dgraph.graphql.schema_created_at", + ValueType: pb.Posting_DATETIME, + }, + }, }) if all || x.WorkerConfig.AclEnabled { @@ -661,6 +672,12 @@ func initialSchemaInternal(all bool) []*pb.SchemaUpdate { Directive: pb.SchemaUpdate_INDEX, Tokenizer: []string{"exact"}, Upsert: true, + }, &pb.SchemaUpdate{ + Predicate: "dgraph.graphql.schema_history", + ValueType: pb.Posting_STRING, + }, &pb.SchemaUpdate{ + Predicate: "dgraph.graphql.schema_created_at", + ValueType: pb.Posting_DATETIME, }) if all || x.WorkerConfig.AclEnabled { diff --git a/systest/backup/encryption/backup_test.go b/systest/backup/encryption/backup_test.go index a004dc692aa..502d92f3f61 100644 --- a/systest/backup/encryption/backup_test.go +++ b/systest/backup/encryption/backup_test.go @@ -295,11 +295,12 @@ func runRestore(t *testing.T, lastDir string, commitTs uint64) map[string]string restoredPreds, err := testutil.GetPredicateNames(pdir) require.NoError(t, err) require.ElementsMatch(t, []string{"dgraph.graphql.schema", "dgraph.cors", "dgraph.graphql.xid", - "dgraph.type", "movie"}, restoredPreds) + "dgraph.type", "movie", "dgraph.graphql.schema_history", "dgraph.graphql.schema_created_at"}, + restoredPreds) restoredTypes, err := testutil.GetTypeNames(pdir) require.NoError(t, err) - require.ElementsMatch(t, []string{"Node", "dgraph.graphql"}, restoredTypes) + require.ElementsMatch(t, []string{"Node", "dgraph.graphql", "dgraph.graphql.history"}, restoredTypes) require.NoError(t, err) t.Logf("--- Restored values: %+v\n", restored) diff --git a/systest/backup/filesystem/backup_test.go b/systest/backup/filesystem/backup_test.go index 98caf5fd02e..c6df6ce966b 100644 --- a/systest/backup/filesystem/backup_test.go +++ b/systest/backup/filesystem/backup_test.go @@ -108,8 +108,9 @@ func TestBackupFilesystem(t *testing.T) { // Check the predicates and types in the schema are as expected. // TODO: refactor tests so that minio and filesystem tests share most of their logic. - preds := []string{"dgraph.graphql.schema", "dgraph.cors", "dgraph.graphql.xid", "dgraph.type", "movie"} - types := []string{"Node", "dgraph.graphql"} + preds := []string{"dgraph.graphql.schema", "dgraph.cors", "dgraph.graphql.xid", "dgraph.type", "movie", + "dgraph.graphql.schema_history", "dgraph.graphql.schema_created_at"} + types := []string{"Node", "dgraph.graphql", "dgraph.graphql.history"} testutil.CheckSchema(t, preds, types) checks := []struct { diff --git a/systest/backup/minio/backup_test.go b/systest/backup/minio/backup_test.go index ccdfa119bb2..570260b0ed4 100644 --- a/systest/backup/minio/backup_test.go +++ b/systest/backup/minio/backup_test.go @@ -114,8 +114,9 @@ func TestBackupMinio(t *testing.T) { // Check the predicates and types in the schema are as expected. // TODO: refactor tests so that minio and filesystem tests share most of their logic. - preds := []string{"dgraph.graphql.schema", "dgraph.cors", "dgraph.graphql.xid", "dgraph.type", "movie"} - types := []string{"Node", "dgraph.graphql"} + preds := []string{"dgraph.graphql.schema", "dgraph.cors", "dgraph.graphql.xid", "dgraph.type", "movie", + "dgraph.graphql.schema_history", "dgraph.graphql.schema_created_at"} + types := []string{"Node", "dgraph.graphql", "dgraph.graphql.history"} testutil.CheckSchema(t, preds, types) checks := []struct { diff --git a/systest/export/export_test.go b/systest/export/export_test.go index ac21fb1e1a4..4cd122462b3 100644 --- a/systest/export/export_test.go +++ b/systest/export/export_test.go @@ -81,6 +81,8 @@ var expectedSchema = `:string .` + " " + ` :[string] @index(exact) .` + " " + ` :string @index(exact) @upsert .` + " " + ` :string .` + " " + ` +:string .` + " " + ` +:datetime .` + " " + ` type Node { movie } @@ -88,6 +90,10 @@ type dgraph.graphql { dgraph.graphql.schema dgraph.graphql.xid } +type dgraph.graphql.history { + dgraph.graphql.schema_history + dgraph.graphql.schema_created_at +} ` func setupDgraph(t *testing.T) { diff --git a/systest/queries_test.go b/systest/queries_test.go index 66f204c34cb..1c452262278 100644 --- a/systest/queries_test.go +++ b/systest/queries_test.go @@ -392,6 +392,12 @@ func SchemaQueryTestPredicate1(t *testing.T, c *dgo.Dgraph) { { "predicate": "dgraph.cors" }, + { + "predicate": "dgraph.graphql.schema_history" + }, + { + "predicate": "dgraph.graphql.schema_created_at" + }, { "predicate": "dgraph.xid" }, diff --git a/worker/export.go b/worker/export.go index 7d334547ed8..3c762b33214 100644 --- a/worker/export.go +++ b/worker/export.go @@ -649,7 +649,10 @@ func export(ctx context.Context, in *pb.ExportRequest) (ExportedFiles, error) { // Ignore this predicate. case pk.Attr == "dgraph.cors": // Ignore this predicate. - + case pk.Attr == "dgraph.graphql.schema_created_at": + // Ignore this predicate. + case pk.Attr == "dgraph.graphql.schema_history": + // Ignore this predicate. case pk.IsData() && pk.Attr == "dgraph.graphql.schema": // Export the graphql schema. pl, err := posting.ReadPostingList(key, itr) diff --git a/x/keys.go b/x/keys.go index e9f86e86671..a980de04d4a 100644 --- a/x/keys.go +++ b/x/keys.go @@ -529,7 +529,6 @@ func Parse(key []byte) (ParsedKey, error) { // These predicates appear for queries that have * as predicate in them. var starAllPredicateMap = map[string]struct{}{ "dgraph.type": {}, - "dgraph.cors": {}, } var aclPredicateMap = map[string]struct{}{ @@ -542,8 +541,11 @@ var aclPredicateMap = map[string]struct{}{ } var graphqlReservedPredicate = map[string]struct{}{ - "dgraph.graphql.xid": {}, - "dgraph.graphql.schema": {}, + "dgraph.graphql.xid": {}, + "dgraph.graphql.schema": {}, + "dgraph.cors": {}, + "dgraph.graphql.schema_history": {}, + "dgraph.graphql.schema_created_at": {}, } // internalPredicateMap stores a set of Dgraph's internal predicate. An internal @@ -554,10 +556,11 @@ var internalPredicateMap = map[string]struct{}{ } var preDefinedTypeMap = map[string]struct{}{ - "dgraph.graphql": {}, - "dgraph.type.User": {}, - "dgraph.type.Group": {}, - "dgraph.type.Rule": {}, + "dgraph.graphql": {}, + "dgraph.type.User": {}, + "dgraph.type.Group": {}, + "dgraph.type.Rule": {}, + "dgraph.graphql.history": {}, } // IsGraphqlReservedPredicate returns true if it is the predicate is reserved by graphql. diff --git a/x/x.go b/x/x.go index cdd1a9988d2..828896877fd 100644 --- a/x/x.go +++ b/x/x.go @@ -139,6 +139,9 @@ const ( },{ "fields": [{"name": "dgraph.rule.predicate"},{"name": "dgraph.rule.permission"}], "name": "dgraph.type.Rule" +}, { + "fields": [{"name": "dgraph.graphql.schema_history"},{"name": "dgraph.graphql.schema_created_at"}], + "name": "dgraph.graphql.history" }]` // GroupIdFileName is the name of the file storing the ID of the group to which @@ -155,6 +158,8 @@ const ( // GraphqlPredicates is the json representation of the predicate reserved for graphql system. GraphqlPredicates = ` {"predicate":"dgraph.graphql.schema", "type": "string"}, +{"predicate":"dgraph.graphql.schema_history", "type": "string"}, +{"predicate":"dgraph.graphql.schema_created_at", "type": "datetime"}, {"predicate":"dgraph.graphql.xid","type":"string","index":true,"tokenizer":["exact"],"upsert":true} ` )