diff --git a/CHANGELOG.md b/CHANGELOG.md index fa29af5f089..5cc22afd2e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project will adhere to [Calendar Versioning](https://calver.org/) starting v20.03. -## [20.07.1] - Unreleased +## [20.07.1] - 2020-09-17 [20.07.1]: https://github.com/dgraph-io/dgraph/compare/v20.07.0...v20.07.1 ### Changed @@ -19,6 +19,9 @@ and this project will adhere to [Calendar Versioning](https://calver.org/) start - GraphQL - Adds auth for subscriptions. ([#6165][]) +- Add --cache_mb and --cache_percentage flags. ([#6286][]) +- Add flags to set table and vlog loading mode for zero. ([#6342][]) +- Add flag to set up compression in zero. ([#6355][]) ### Fixed @@ -33,12 +36,46 @@ and this project will adhere to [Calendar Versioning](https://calver.org/) start - Don't reserve certain queries/mutations/inputs when a type is remote. ([#6201][]) - Linking of xids for deep mutations. ([#6203][]) - Prevent empty values in fields having `id` directive. ([#6196][]) + - Fixes unexpected fragment behaviour. ([#6274][]) + - Incorrect generatedSchema in update GQLSchema. ([#6354][]) - Fix out of order issues with split keys in bulk loader. ([#6124][]) - Rollup a batch if more than 2 seconds elapsed since last batch. ([#6137][]) - Refactor: Simplify how list splits are tracked. ([#6070][]) - Fix: Don't allow idx flag to be set to 0 on dgraph zero. ([#6192][]) - Fix error message for idx = 0 for dgraph zero. ([#6199][]) - +- Stop forcing RAM mode for the write-ahead log. ([#6259][]) +- Fix panicwrap parent check. ([#6299][]) +- Sort manifests by BackupNum in file handler. ([#6279][]) +- Fixes queries which use variable at the top level. ([#6290][]) +- Return error on closed DB. ([#6320][]) +- Optimize splits by doing binary search. Clear the pack from the main list. ([#6332][]) +- Proto fix needed for PR [#6331][]. ([#6346][]) +- Sentry nil pointer check. ([#6374][]) +- Don't store start_ts in postings. ([#6213][]) +- Use z.Closer instead of y.Closer. ([#6399][]) +- Make Alpha Shutdown Again. ([#6402][]) +- Force exit if CTRL-C is caught before initialization. ([#6407][]) +- Update advanced-queries.md. +- Batch list in bulk loader to avoid panic. ([#6446][]) +- Enterprise features + - Make backups cancel other tasks. ([#6243][]) + - Online Restore honors credentials passed in. ([#6302][]) + - Add a lock to backups to process one request at a time. ([#6339][]) + - Fix Star_All delete query when used with ACL enabled. ([#6336][]) + +[#6407]: https://github.com/dgraph-io/dgraph/issues/6407 +[#6336]: https://github.com/dgraph-io/dgraph/issues/6336 +[#6446]: https://github.com/dgraph-io/dgraph/issues/6446 +[#6402]: https://github.com/dgraph-io/dgraph/issues/6402 +[#6399]: https://github.com/dgraph-io/dgraph/issues/6399 +[#6346]: https://github.com/dgraph-io/dgraph/issues/6346 +[#6332]: https://github.com/dgraph-io/dgraph/issues/6332 +[#6243]: https://github.com/dgraph-io/dgraph/issues/6243 +[#6302]: https://github.com/dgraph-io/dgraph/issues/6302 +[#6339]: https://github.com/dgraph-io/dgraph/issues/6339 +[#6355]: https://github.com/dgraph-io/dgraph/issues/6355 +[#6342]: https://github.com/dgraph-io/dgraph/issues/6342 +[#6286]: https://github.com/dgraph-io/dgraph/issues/6286 [#6201]: https://github.com/dgraph-io/dgraph/issues/6201 [#6203]: https://github.com/dgraph-io/dgraph/issues/6203 [#6196]: https://github.com/dgraph-io/dgraph/issues/6196 @@ -61,6 +98,16 @@ and this project will adhere to [Calendar Versioning](https://calver.org/) start [#6098]: https://github.com/dgraph-io/dgraph/issues/6098 [#6151]: https://github.com/dgraph-io/dgraph/issues/6151 [#6165]: https://github.com/dgraph-io/dgraph/issues/6165 +[#6259]: https://github.com/dgraph-io/dgraph/issues/6259 +[#6299]: https://github.com/dgraph-io/dgraph/issues/6299 +[#6279]: https://github.com/dgraph-io/dgraph/issues/6279 +[#6290]: https://github.com/dgraph-io/dgraph/issues/6290 +[#6274]: https://github.com/dgraph-io/dgraph/issues/6274 +[#6320]: https://github.com/dgraph-io/dgraph/issues/6320 +[#6331]: https://github.com/dgraph-io/dgraph/issues/6331 +[#6354]: https://github.com/dgraph-io/dgraph/issues/6354 +[#6374]: https://github.com/dgraph-io/dgraph/issues/6374 +[#6213]: https://github.com/dgraph-io/dgraph/issues/6213 ## [20.07.0] - 2020-07-28 [20.07.0]: https://github.com/dgraph-io/dgraph/compare/v20.03.4...v20.07.0 diff --git a/conn/node.go b/conn/node.go index a27fecab972..01416f0400c 100644 --- a/conn/node.go +++ b/conn/node.go @@ -38,6 +38,7 @@ import ( "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/raftwal" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" ) var ( @@ -647,7 +648,7 @@ func (n *Node) WaitLinearizableRead(ctx context.Context) error { } // RunReadIndexLoop runs the RAFT index in a loop. -func (n *Node) RunReadIndexLoop(closer *y.Closer, readStateCh <-chan raft.ReadState) { +func (n *Node) RunReadIndexLoop(closer *z.Closer, readStateCh <-chan raft.ReadState) { defer closer.Done() readIndex := func(activeRctx []byte) (uint64, error) { // Read Request can get rejected then we would wait indefinitely on the channel diff --git a/conn/pool.go b/conn/pool.go index d81d3d36812..a822b2a4474 100644 --- a/conn/pool.go +++ b/conn/pool.go @@ -21,10 +21,10 @@ import ( "sync" "time" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgo/v200/protos/api" "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" "github.com/golang/glog" "github.com/pkg/errors" "go.opencensus.io/plugin/ocgrpc" @@ -50,7 +50,7 @@ type Pool struct { lastEcho time.Time Addr string - closer *y.Closer + closer *z.Closer healthInfo pb.HealthInfo } @@ -175,7 +175,7 @@ func newPool(addr string) (*Pool, error) { if err != nil { return nil, err } - pl := &Pool{conn: conn, Addr: addr, lastEcho: time.Now(), closer: y.NewCloser(1)} + pl := &Pool{conn: conn, Addr: addr, lastEcho: time.Now(), closer: z.NewCloser(1)} go pl.MonitorHealth() return pl, nil } diff --git a/contrib/scripts/functions.sh b/contrib/scripts/functions.sh index f2ac6c14a76..663124db251 100755 --- a/contrib/scripts/functions.sh +++ b/contrib/scripts/functions.sh @@ -37,7 +37,7 @@ function restartCluster { fi docker ps -a --filter label="cluster=test" --format "{{.Names}}" | xargs -r docker rm -f - GOPATH=$docker_compose_gopath docker-compose -p dgraph -f $compose_file up --force-recreate --remove-orphans -d || exit 1 + GOPATH=$docker_compose_gopath docker-compose -p dgraph -f $compose_file up --force-recreate --build --remove-orphans -d || exit 1 popd >/dev/null $basedir/contrib/wait-for-it.sh -t 60 localhost:6180 || exit 1 diff --git a/dgraph/Makefile b/dgraph/Makefile index d734727628d..fad803d380e 100644 --- a/dgraph/Makefile +++ b/dgraph/Makefile @@ -16,7 +16,7 @@ BIN = dgraph BUILD ?= $(shell git rev-parse --short HEAD) -BUILD_CODENAME = shuri +BUILD_CODENAME = shuri-1 BUILD_DATE ?= $(shell git log -1 --format=%ci) BUILD_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) BUILD_VERSION ?= $(shell git describe --always --tags) diff --git a/dgraph/cmd/alpha/run.go b/dgraph/cmd/alpha/run.go index 84928904b39..dd0348136fd 100644 --- a/dgraph/cmd/alpha/run.go +++ b/dgraph/cmd/alpha/run.go @@ -35,7 +35,6 @@ import ( "time" badgerpb "github.com/dgraph-io/badger/v2/pb" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgo/v200/protos/api" "github.com/dgraph-io/dgraph/edgraph" "github.com/dgraph-io/dgraph/ee/enc" @@ -47,6 +46,7 @@ import ( "github.com/dgraph-io/dgraph/tok" "github.com/dgraph-io/dgraph/worker" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" "github.com/golang/glog" "github.com/pkg/errors" "github.com/spf13/cast" @@ -80,6 +80,8 @@ var ( // need this here to refer it in admin_backup.go adminServer web.IServeGraphQL + + initDone uint32 ) func init() { @@ -105,15 +107,24 @@ they form a Raft group and provide synchronous replication. flag.StringP("postings", "p", "p", "Directory to store posting lists.") // Options around how to set up Badger. - flag.String("badger.tables", "mmap", - "[ram, mmap, disk] Specifies how Badger LSM tree is stored. "+ - "Option sequence consume most to least RAM while providing best to worst read "+ - "performance respectively.") - flag.String("badger.vlog", "mmap", - "[mmap, disk] Specifies how Badger Value log is stored."+ - " mmap consumes more RAM, but provides better performance.") - flag.Int("badger.compression_level", 3, - "The compression level for Badger. A higher value uses more resources.") + flag.String("badger.tables", "mmap,mmap", + "[ram, mmap, disk] Specifies how Badger LSM tree is stored for the postings and "+ + "write-ahead directory. Option sequence consume most to least RAM while providing "+ + "best to worst read performance respectively. If you pass two values separated by a "+ + "comma, the first value will be used for the postings directory and the second for "+ + "the write-ahead log directory.") + flag.String("badger.vlog", "mmap,mmap", + "[mmap, disk] Specifies how Badger Value log is stored for the postings and write-ahead "+ + "log directory. mmap consumes more RAM, but provides better performance. If you pass "+ + "two values separated by a comma the first value will be used for the postings "+ + "directory and the second for the w directory.") + flag.String("badger.compression_level", "3,0", + "Specifies the compression level for the postings and write-ahead log "+ + "directory. A higher value uses more resources. The value of 0 disables "+ + "compression. If you pass two values separated by a comma the first "+ + "value will be used for the postings directory (p) and the second for "+ + "the wal directory (w). If a single value is passed the value is used "+ + "as compression level for both directories.") enc.RegisterFlags(flag) // Snapshot and Transactions. @@ -196,9 +207,21 @@ they form a Raft group and provide synchronous replication. grpc.EnableTracing = false flag.Bool("graphql_introspection", true, "Set to false for no GraphQL schema introspection") + flag.Bool("graphql_debug", false, "Enable debug mode in GraphQL. "+ + "This returns auth errors to clients. We do not recommend turning it on for production.") flag.Bool("ludicrous_mode", false, "Run alpha in ludicrous mode") flag.Bool("graphql_extensions", true, "Set to false if extensions not required in GraphQL response body") flag.Duration("graphql_poll_interval", time.Second, "polling interval for graphql subscription.") + + // Cache flags + flag.Int64("cache_mb", 0, "Total size of cache (in MB) to be used in alpha.") + // TODO(Naman): The PostingListCache is a no-op for now. Once the posting list cache is + // added in release branch, use it. + flag.String("cache_percentage", "0,65,25,0,10", + `Cache percentages summing up to 100 for various caches (FORMAT: + PostingListCache,PstoreBlockCache,PstoreIndexCache,WstoreBlockCache,WstoreIndexCache). + PostingListCache should be 0 and is a no-op. + `) } func setupCustomTokenizers() { @@ -375,7 +398,7 @@ func setupListener(addr string, port int) (net.Listener, error) { return net.Listen("tcp", fmt.Sprintf("%s:%d", addr, port)) } -func serveGRPC(l net.Listener, tlsCfg *tls.Config, closer *y.Closer) { +func serveGRPC(l net.Listener, tlsCfg *tls.Config, closer *z.Closer) { defer closer.Done() x.RegisterExporters(Alpha.Conf, "dgraph.alpha") @@ -398,7 +421,7 @@ func serveGRPC(l net.Listener, tlsCfg *tls.Config, closer *y.Closer) { s.Stop() } -func serveHTTP(l net.Listener, tlsCfg *tls.Config, closer *y.Closer) { +func serveHTTP(l net.Listener, tlsCfg *tls.Config, closer *z.Closer) { defer closer.Done() srv := &http.Server{ ReadTimeout: 10 * time.Second, @@ -421,7 +444,7 @@ func serveHTTP(l net.Listener, tlsCfg *tls.Config, closer *y.Closer) { } } -func setupServer(closer *y.Closer) { +func setupServer(closer *z.Closer) { go worker.RunServer(bindall) // For pb.communication. laddr := "localhost" @@ -533,7 +556,7 @@ func setupServer(closer *y.Closer) { http.HandleFunc("/ui/keywords", keywordHandler) // Initialize the servers. - admin.ServerCloser = y.NewCloser(3) + admin.ServerCloser = z.NewCloser(3) go serveGRPC(grpcListener, tlsCfg, admin.ServerCloser) go serveHTTP(httpListener, tlsCfg, admin.ServerCloser) @@ -559,6 +582,7 @@ func setupServer(closer *y.Closer) { glog.Infoln("gRPC server started. Listening on port", grpcPort()) glog.Infoln("HTTP server started. Listening on port", httpPort()) + atomic.AddUint32(&initDone, 1) admin.ServerCloser.Wait() } @@ -573,18 +597,67 @@ func run() { } bindall = Alpha.Conf.GetBool("bindall") + totalCache := int64(Alpha.Conf.GetInt("cache_mb")) + x.AssertTruef(totalCache >= 0, "ERROR: Cache size must be non-negative") + + cachePercentage := Alpha.Conf.GetString("cache_percentage") + cachePercent, err := x.GetCachePercentages(cachePercentage, 5) + x.Check(err) + // TODO(Naman): PostingListCache doesn't exist now. + postingListCacheSize := (cachePercent[0] * (totalCache << 20)) / 100 + x.AssertTruef(postingListCacheSize == 0, "ERROR: postingListCacheSize should be 0.") + pstoreBlockCacheSize := (cachePercent[1] * (totalCache << 20)) / 100 + pstoreIndexCacheSize := (cachePercent[2] * (totalCache << 20)) / 100 + wstoreBlockCacheSize := (cachePercent[3] * (totalCache << 20)) / 100 + wstoreIndexCacheSize := (cachePercent[4] * (totalCache << 20)) / 100 + + compressionLevelString := Alpha.Conf.GetString("badger.compression_level") + compressionLevels, err := x.GetCompressionLevels(compressionLevelString) + x.Check(err) + postingDirCompressionLevel := compressionLevels[0] + walDirCompressionLevel := compressionLevels[1] + opts := worker.Options{ - BadgerTables: Alpha.Conf.GetString("badger.tables"), - BadgerVlog: Alpha.Conf.GetString("badger.vlog"), - BadgerCompressionLevel: Alpha.Conf.GetInt("badger.compression_level"), - PostingDir: Alpha.Conf.GetString("postings"), - WALDir: Alpha.Conf.GetString("wal"), + PostingDir: Alpha.Conf.GetString("postings"), + WALDir: Alpha.Conf.GetString("wal"), + PostingDirCompressionLevel: postingDirCompressionLevel, + WALDirCompressionLevel: walDirCompressionLevel, + PBlockCacheSize: pstoreBlockCacheSize, + PIndexCacheSize: pstoreIndexCacheSize, + WBlockCacheSize: wstoreBlockCacheSize, + WIndexCacheSize: wstoreIndexCacheSize, MutationsMode: worker.AllowMutations, AuthToken: Alpha.Conf.GetString("auth_token"), AllottedMemory: Alpha.Conf.GetFloat64("lru_mb"), } + badgerTables := strings.Split(Alpha.Conf.GetString("badger.tables"), ",") + if len(badgerTables) != 1 && len(badgerTables) != 2 { + glog.Fatalf("Unable to read badger.tables options. Expected single value or two "+ + "comma-separated values. Got %s", Alpha.Conf.GetString("badger.tables")) + } + if len(badgerTables) == 1 { + opts.BadgerTables = badgerTables[0] + opts.BadgerWalTables = badgerTables[0] + } else { + opts.BadgerTables = badgerTables[0] + opts.BadgerWalTables = badgerTables[1] + } + + badgerVlog := strings.Split(Alpha.Conf.GetString("badger.vlog"), ",") + if len(badgerVlog) != 1 && len(badgerVlog) != 2 { + glog.Fatalf("Unable to read badger.vlog options. Expected single value or two "+ + "comma-separated values. Got %s", Alpha.Conf.GetString("badger.vlog")) + } + if len(badgerVlog) == 1 { + opts.BadgerVlog = badgerVlog[0] + opts.BadgerWalVlog = badgerVlog[0] + } else { + opts.BadgerVlog = badgerVlog[0] + opts.BadgerWalVlog = badgerVlog[1] + } + secretFile := Alpha.Conf.GetString("acl_secret_file") if secretFile != "" { hmacSecret, err := ioutil.ReadFile(secretFile) @@ -651,6 +724,7 @@ func run() { x.Config.NormalizeNodeLimit = cast.ToInt(Alpha.Conf.GetString("normalize_node_limit")) x.Config.PollInterval = Alpha.Conf.GetDuration("graphql_poll_interval") x.Config.GraphqlExtension = Alpha.Conf.GetBool("graphql_extensions") + x.Config.GraphqlDebug = Alpha.Conf.GetBool("graphql_debug") x.PrintVersion() glog.Infof("x.Config: %+v", x.Config) @@ -696,55 +770,72 @@ func run() { } numShutDownSig++ glog.Infoln("Caught Ctrl-C. Terminating now (this may take a few seconds)...") - if numShutDownSig == 3 { + + switch { + case atomic.LoadUint32(&initDone) < 2: + // Forcefully kill alpha if we haven't finish server initialization. + glog.Infoln("Stopped before initialization completed") + os.Exit(1) + case numShutDownSig == 3: glog.Infoln("Signaled thrice. Aborting!") os.Exit(1) } } }() - // Setup external communication. - aclCloser := y.NewCloser(1) + updaters := z.NewCloser(4) go func() { worker.StartRaftNodes(worker.State.WALstore, bindall) + atomic.AddUint32(&initDone, 1) + // initialization of the admin account can only be done after raft nodes are running // and health check passes - edgraph.ResetAcl() - edgraph.RefreshAcls(aclCloser) - edgraph.ResetCors() + edgraph.ResetAcl(updaters) + edgraph.RefreshAcls(updaters) + edgraph.ResetCors(updaters) // Update the accepted cors origins. - for { - origins, err := edgraph.GetCorsOrigins(context.TODO()) + for updaters.Ctx().Err() == nil { + origins, err := edgraph.GetCorsOrigins(updaters.Ctx()) if err != nil { glog.Errorf("Error while retriving cors origins: %s", err.Error()) continue } x.UpdateCorsOrigins(origins) - break + return } }() // Listen for any new cors origin update. - corsCloser := y.NewCloser(1) - go listenForCorsUpdate(corsCloser) + go listenForCorsUpdate(updaters) // Graphql subscribes to alpha to get schema updates. We need to close that before we // close alpha. This closer is for closing and waiting that subscription. - adminCloser := y.NewCloser(1) + adminCloser := z.NewCloser(1) setupServer(adminCloser) glog.Infoln("GRPC and HTTP stopped.") - aclCloser.SignalAndWait() - corsCloser.SignalAndWait() + + // This might not close until group is given the signal to close. So, only signal here, + // wait for it after group is closed. + updaters.Signal() + worker.BlockingStop() + glog.Infoln("worker stopped.") + adminCloser.SignalAndWait() - glog.Info("Disposing server state.") + glog.Infoln("adminCloser closed.") + worker.State.Dispose() x.RemoveCidFile() + glog.Info("worker.State disposed.") + + updaters.Wait() + glog.Infoln("updaters closed.") + glog.Infoln("Server shutdown. Bye!") } // listenForCorsUpdate listen for any cors change and update the accepeted cors. -func listenForCorsUpdate(closer *y.Closer) { +func listenForCorsUpdate(closer *z.Closer) { prefix := x.DataKey("dgraph.cors", 0) // Remove uid from the key, to get the correct prefix prefix = prefix[:len(prefix)-8] diff --git a/dgraph/cmd/bulk/reduce.go b/dgraph/cmd/bulk/reduce.go index 880f3242699..84a85b1d7d1 100644 --- a/dgraph/cmd/bulk/reduce.go +++ b/dgraph/cmd/bulk/reduce.go @@ -43,6 +43,7 @@ import ( "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/worker" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" ) type reducer struct { @@ -84,10 +85,10 @@ func (r *reducer) run() error { splitWriter := tmpDb.NewManagedWriteBatch() ci := &countIndexer{ - reducer: r, - writer: writer, + reducer: r, + writer: writer, splitWriter: splitWriter, - tmpDb: tmpDb, + tmpDb: tmpDb, } sort.Slice(partitionKeys, func(i, j int) bool { return bytes.Compare(partitionKeys[i], partitionKeys[j]) < 0 @@ -130,7 +131,7 @@ func (r *reducer) createBadgerInternal(dir string, compression bool) *badger.DB opt := badger.DefaultOptions(dir).WithSyncWrites(false). WithTableLoadingMode(bo.MemoryMap).WithValueThreshold(1 << 10 /* 1 KB */). - WithLogger(nil).WithMaxCacheSize(1 << 20). + WithLogger(nil).WithBlockCacheSize(1 << 20). WithEncryptionKey(r.opt.EncryptionKey).WithCompression(bo.None) // Overwrite badger options based on the options provided by the user. @@ -333,7 +334,7 @@ func (r *reducer) streamIdFor(pred string) uint32 { return streamId } -func (r *reducer) encode(entryCh chan *encodeRequest, closer *y.Closer) { +func (r *reducer) encode(entryCh chan *encodeRequest, closer *z.Closer) { defer closer.Done() for req := range entryCh { @@ -383,7 +384,7 @@ func (r *reducer) writeTmpSplits(ci *countIndexer, kvsCh chan *bpb.KVList, wg *s x.Check(ci.splitWriter.Flush()) } -func (r *reducer) startWriting(ci *countIndexer, writerCh chan *encodeRequest, closer *y.Closer) { +func (r *reducer) startWriting(ci *countIndexer, writerCh chan *encodeRequest, closer *z.Closer) { defer closer.Done() // Concurrently write split lists to a temporary badger. @@ -401,7 +402,16 @@ func (r *reducer) startWriting(ci *countIndexer, writerCh chan *encodeRequest, c // Wait for it to be encoded. start := time.Now() - x.Check(ci.writer.Write(req.list)) + for len(req.list.GetKv()) > 0 { + batchSize := 100 + if len(req.list.Kv) < batchSize { + batchSize = len(req.list.Kv) + } + batch := &bpb.KVList{Kv: req.list.Kv[:batchSize]} + req.list.Kv = req.list.Kv[batchSize:] + x.Check(ci.writer.Write(batch)) + } + if req.splitList != nil && len(req.splitList.Kv) > 0 { splitCh <- req.splitList } @@ -456,14 +466,14 @@ func (r *reducer) reduce(partitionKeys [][]byte, mapItrs []*mapIterator, ci *cou fmt.Printf("Num CPUs: %d\n", cpu) encoderCh := make(chan *encodeRequest, 2*cpu) writerCh := make(chan *encodeRequest, 2*cpu) - encoderCloser := y.NewCloser(cpu) + encoderCloser := z.NewCloser(cpu) for i := 0; i < cpu; i++ { // Start listening to encode entries // For time being let's lease 100 stream id for each encoder. go r.encode(encoderCh, encoderCloser) } // Start listening to write the badger list. - writerCloser := y.NewCloser(1) + writerCloser := z.NewCloser(1) go r.startWriting(ci, writerCh, writerCloser) for i := 0; i < len(partitionKeys); i++ { diff --git a/dgraph/cmd/counter/.gitignore b/dgraph/cmd/increment/.gitignore similarity index 100% rename from dgraph/cmd/counter/.gitignore rename to dgraph/cmd/increment/.gitignore diff --git a/dgraph/cmd/counter/increment.go b/dgraph/cmd/increment/increment.go similarity index 98% rename from dgraph/cmd/counter/increment.go rename to dgraph/cmd/increment/increment.go index 8bddcf69621..97ca43ce035 100644 --- a/dgraph/cmd/counter/increment.go +++ b/dgraph/cmd/increment/increment.go @@ -14,10 +14,10 @@ * limitations under the License. */ -// Package counter builds a tool that retrieves a value for UID=0x01, and increments +// Package increment builds a tool that retrieves a value for UID=0x01, and increments // it by 1. If successful, it prints out the incremented value. It assumes that it has // access to UID=0x01, and that `val` predicate is of type int. -package counter +package increment import ( "context" diff --git a/dgraph/cmd/counter/increment_test.go b/dgraph/cmd/increment/increment_test.go similarity index 99% rename from dgraph/cmd/counter/increment_test.go rename to dgraph/cmd/increment/increment_test.go index f79451e1b97..afc3a88de74 100644 --- a/dgraph/cmd/counter/increment_test.go +++ b/dgraph/cmd/increment/increment_test.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package counter +package increment import ( "context" diff --git a/dgraph/cmd/root.go b/dgraph/cmd/root.go index e6a89077e16..80892d095c9 100644 --- a/dgraph/cmd/root.go +++ b/dgraph/cmd/root.go @@ -25,9 +25,9 @@ import ( "github.com/dgraph-io/dgraph/dgraph/cmd/bulk" "github.com/dgraph-io/dgraph/dgraph/cmd/cert" "github.com/dgraph-io/dgraph/dgraph/cmd/conv" - "github.com/dgraph-io/dgraph/dgraph/cmd/counter" "github.com/dgraph-io/dgraph/dgraph/cmd/debug" "github.com/dgraph-io/dgraph/dgraph/cmd/debuginfo" + "github.com/dgraph-io/dgraph/dgraph/cmd/increment" "github.com/dgraph-io/dgraph/dgraph/cmd/live" "github.com/dgraph-io/dgraph/dgraph/cmd/migrate" "github.com/dgraph-io/dgraph/dgraph/cmd/version" @@ -75,7 +75,7 @@ var rootConf = viper.New() // subcommands initially contains all default sub-commands. var subcommands = []*x.SubCommand{ &bulk.Bulk, &cert.Cert, &conv.Conv, &live.Live, &alpha.Alpha, &zero.Zero, &version.Version, - &debug.Debug, &counter.Increment, &migrate.Migrate, &debuginfo.DebugInfo, &upgrade.Upgrade, + &debug.Debug, &increment.Increment, &migrate.Migrate, &debuginfo.DebugInfo, &upgrade.Upgrade, } func initCmds() { diff --git a/dgraph/cmd/zero/license.go b/dgraph/cmd/zero/license.go index 84f0c75e4b6..6de7366fe6b 100644 --- a/dgraph/cmd/zero/license.go +++ b/dgraph/cmd/zero/license.go @@ -21,8 +21,8 @@ package zero import ( "net/http" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgraph/protos/pb" + "github.com/dgraph-io/ristretto/z" ) // dummy function as enterprise features are not available in oss binary. @@ -31,7 +31,7 @@ func (n *node) proposeTrialLicense() error { } // periodically checks the validity of the enterprise license and updates the membership state. -func (n *node) updateEnterpriseState(closer *y.Closer) { +func (n *node) updateEnterpriseState(closer *z.Closer) { closer.Done() } diff --git a/dgraph/cmd/zero/license_ee.go b/dgraph/cmd/zero/license_ee.go index 29ff9f64c70..95f8ef3c3d9 100644 --- a/dgraph/cmd/zero/license_ee.go +++ b/dgraph/cmd/zero/license_ee.go @@ -20,9 +20,9 @@ import ( "net/http" "time" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" humanize "github.com/dustin/go-humanize" "github.com/gogo/protobuf/proto" "github.com/golang/glog" @@ -61,7 +61,7 @@ func (s *Server) expireLicense() { // periodically checks the validity of the enterprise license and // 1. Sets license.Enabled to false in membership state if license has expired. // 2. Prints out warning once every day a week before the license is set to expire. -func (n *node) updateEnterpriseState(closer *y.Closer) { +func (n *node) updateEnterpriseState(closer *z.Closer) { defer closer.Done() interval := 5 * time.Second diff --git a/dgraph/cmd/zero/raft.go b/dgraph/cmd/zero/raft.go index cd82893353d..1a549447362 100644 --- a/dgraph/cmd/zero/raft.go +++ b/dgraph/cmd/zero/raft.go @@ -28,10 +28,10 @@ import ( otrace "go.opencensus.io/trace" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgraph/conn" "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" farm "github.com/dgryski/go-farm" "github.com/golang/glog" "github.com/google/uuid" @@ -44,7 +44,7 @@ type node struct { *conn.Node server *Server ctx context.Context - closer *y.Closer // to stop Run. + closer *z.Closer // to stop Run. // The last timestamp when this Zero was able to reach quorum. mu sync.RWMutex @@ -582,7 +582,7 @@ func (n *node) initAndStartNode() error { return nil } -func (n *node) updateZeroMembershipPeriodically(closer *y.Closer) { +func (n *node) updateZeroMembershipPeriodically(closer *z.Closer) { defer closer.Done() ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() @@ -599,7 +599,7 @@ func (n *node) updateZeroMembershipPeriodically(closer *y.Closer) { var startOption = otrace.WithSampler(otrace.ProbabilitySampler(0.01)) -func (n *node) checkQuorum(closer *y.Closer) { +func (n *node) checkQuorum(closer *z.Closer) { defer closer.Done() ticker := time.NewTicker(time.Second) defer ticker.Stop() @@ -637,7 +637,7 @@ func (n *node) checkQuorum(closer *y.Closer) { } } -func (n *node) snapshotPeriodically(closer *y.Closer) { +func (n *node) snapshotPeriodically(closer *z.Closer) { defer closer.Done() ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() @@ -679,7 +679,7 @@ func (n *node) Run() { // snapshot can cause select loop to block while deleting entries, so run // it in goroutine readStateCh := make(chan raft.ReadState, 100) - closer := y.NewCloser(5) + closer := z.NewCloser(5) defer func() { closer.SignalAndWait() n.closer.Done() diff --git a/dgraph/cmd/zero/run.go b/dgraph/cmd/zero/run.go index e4d62d787b1..e72d5b0d197 100644 --- a/dgraph/cmd/zero/run.go +++ b/dgraph/cmd/zero/run.go @@ -18,7 +18,6 @@ package zero import ( "context" - // "errors" "fmt" "log" "net" @@ -35,12 +34,13 @@ import ( "google.golang.org/grpc" "github.com/dgraph-io/badger/v2" - "github.com/dgraph-io/badger/v2/y" + bopt "github.com/dgraph-io/badger/v2/options" "github.com/dgraph-io/dgraph/conn" "github.com/dgraph-io/dgraph/ee/enc" "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/raftwal" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" "github.com/golang/glog" "github.com/spf13/cobra" ) @@ -55,6 +55,9 @@ type options struct { w string rebalanceInterval time.Duration LudicrousMode bool + + totalCache int64 + cachePercentage string } var opts options @@ -101,6 +104,22 @@ instances to achieve high-availability. " exporter does not support annotation logs and would discard them.") flag.Bool("ludicrous_mode", false, "Run zero in ludicrous mode") flag.String("enterprise_license", "", "Path to the enterprise license file.") + + // Cache flags + flag.Int64("cache_mb", 0, "Total size of cache (in MB) to be used in zero.") + flag.String("cache_percentage", "100,0", + "Cache percentages summing up to 100 for various caches (FORMAT: blockCache,indexCache).") + + // Badger flags + flag.String("badger.tables", "mmap", + "[ram, mmap, disk] Specifies how Badger LSM tree is stored for write-ahead log directory "+ + "write-ahead directory. Option sequence consume most to least RAM while providing "+ + "best to worst read performance respectively") + flag.String("badger.vlog", "mmap", + "[mmap, disk] Specifies how Badger Value log is stored for the write-ahead log directory "+ + "log directory. mmap consumes more RAM, but provides better performance.") + flag.Int("badger.compression_level", 3, + "The compression level for Badger. A higher value uses more resources.") } func setupListener(addr string, port int, kind string) (listener net.Listener, err error) { @@ -132,7 +151,7 @@ func (st *state) serveGRPC(l net.Listener, store *raftwal.DiskStorage) { m.Cfg.DisableProposalForwarding = true st.rs = conn.NewRaftServer(m) - st.node = &node{Node: m, ctx: context.Background(), closer: y.NewCloser(1)} + st.node = &node{Node: m, ctx: context.Background(), closer: z.NewCloser(1)} st.zero = &Server{NumReplicas: opts.numReplicas, Node: st.node} st.zero.Init() st.node.server = st.zero @@ -183,6 +202,8 @@ func run() { w: Zero.Conf.GetString("wal"), rebalanceInterval: Zero.Conf.GetDuration("rebalance_interval"), LudicrousMode: Zero.Conf.GetBool("ludicrous_mode"), + totalCache: int64(Zero.Conf.GetInt("cache_mb")), + cachePercentage: Zero.Conf.GetString("cache_percentage"), } if opts.nodeId == 0 { @@ -231,18 +252,56 @@ func run() { httpListener, err := setupListener(addr, x.PortZeroHTTP+opts.portOffset, "http") x.Check(err) + x.AssertTruef(opts.totalCache >= 0, "ERROR: Cache size must be non-negative") + + cachePercent, err := x.GetCachePercentages(opts.cachePercentage, 2) + x.Check(err) + blockCacheSz := (cachePercent[0] * (opts.totalCache << 20)) / 100 + indexCacheSz := (cachePercent[1] * (opts.totalCache << 20)) / 100 + // Open raft write-ahead log and initialize raft node. x.Checkf(os.MkdirAll(opts.w, 0700), "Error while creating WAL dir.") - kvOpt := badger.LSMOnlyOptions(opts.w).WithSyncWrites(false).WithTruncate(true). - WithValueLogFileSize(64 << 20).WithMaxCacheSize(10 << 20).WithLoadBloomsOnOpen(false) + kvOpt := badger.LSMOnlyOptions(opts.w). + WithSyncWrites(false). + WithTruncate(true). + WithValueLogFileSize(64 << 20). + WithBlockCacheSize(blockCacheSz). + WithIndexCacheSize(indexCacheSz). + WithLoadBloomsOnOpen(false) + + compression_level := Zero.Conf.GetInt("badger.compression_level") + if compression_level > 0 { + // By default, compression is disabled in badger. + kvOpt.Compression = bopt.ZSTD + kvOpt.ZSTDCompressionLevel = compression_level + } - kvOpt.ZSTDCompressionLevel = 3 + // Set loading mode options. + switch Zero.Conf.GetString("badger.tables") { + case "mmap": + kvOpt.TableLoadingMode = bopt.MemoryMap + case "ram": + kvOpt.TableLoadingMode = bopt.LoadToRAM + case "disk": + kvOpt.TableLoadingMode = bopt.FileIO + default: + x.Fatalf("Invalid Badger Tables options") + } + switch Zero.Conf.GetString("badger.vlog") { + case "mmap": + kvOpt.ValueLogLoadingMode = bopt.MemoryMap + case "disk": + kvOpt.ValueLogLoadingMode = bopt.FileIO + default: + x.Fatalf("Invalid Badger Value log options") + } + glog.Infof("Opening zero BadgerDB with options: %+v\n", kvOpt) kv, err := badger.Open(kvOpt) x.Checkf(err, "Error while opening WAL store") defer kv.Close() - gcCloser := y.NewCloser(1) // closer for vLogGC + gcCloser := z.NewCloser(1) // closer for vLogGC go x.RunVlogGC(kv, gcCloser) defer gcCloser.SignalAndWait() @@ -314,5 +373,10 @@ func run() { glog.Infoln("Running Dgraph Zero...") st.zero.closer.Wait() - glog.Infoln("All done.") + glog.Infoln("Closer closed.") + + err = kv.Close() + glog.Infof("Badger closed with err: %v\n", err) + + glog.Infoln("All done. Goodbye!") } diff --git a/dgraph/cmd/zero/zero.go b/dgraph/cmd/zero/zero.go index ed6b2bb184e..9f326fca396 100644 --- a/dgraph/cmd/zero/zero.go +++ b/dgraph/cmd/zero/zero.go @@ -26,12 +26,12 @@ import ( otrace "go.opencensus.io/trace" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgo/v200/protos/api" "github.com/dgraph-io/dgraph/conn" "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/telemetry" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" "github.com/gogo/protobuf/proto" "github.com/golang/glog" "github.com/pkg/errors" @@ -66,7 +66,7 @@ type Server struct { // groupMap map[uint32]*Group nextGroup uint32 leaderChangeCh chan struct{} - closer *y.Closer // Used to tell stream to close. + closer *z.Closer // Used to tell stream to close. connectLock sync.Mutex // Used to serialize connect requests from servers. moveOngoing chan struct{} @@ -89,7 +89,7 @@ func (s *Server) Init() { s.nextTxnTs = 1 s.nextGroup = 1 s.leaderChangeCh = make(chan struct{}, 1) - s.closer = y.NewCloser(2) // grpc and http + s.closer = z.NewCloser(2) // grpc and http s.blockCommitsOn = new(sync.Map) s.moveOngoing = make(chan struct{}, 1) diff --git a/edgraph/access.go b/edgraph/access.go index c79873105fc..879c7d6bbe4 100644 --- a/edgraph/access.go +++ b/edgraph/access.go @@ -21,11 +21,11 @@ package edgraph import ( "context" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgo/v200/protos/api" "github.com/dgraph-io/dgraph/gql" "github.com/dgraph-io/dgraph/query" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" "github.com/golang/glog" ) @@ -42,12 +42,12 @@ func (s *Server) Login(ctx context.Context, } // ResetAcl is an empty method since ACL is only supported in the enterprise version. -func ResetAcl() { +func ResetAcl(closer *z.Closer) { // do nothing } // ResetAcls is an empty method since ACL is only supported in the enterprise version. -func RefreshAcls(closer *y.Closer) { +func RefreshAcls(closer *z.Closer) { // do nothing <-closer.HasBeenClosed() closer.Done() diff --git a/edgraph/access_ee.go b/edgraph/access_ee.go index 0ca838cd4dd..284a6d5b723 100644 --- a/edgraph/access_ee.go +++ b/edgraph/access_ee.go @@ -20,13 +20,12 @@ import ( "time" "github.com/dgraph-io/dgraph/protos/pb" + "github.com/dgraph-io/ristretto/z" "github.com/dgraph-io/dgraph/query" "github.com/pkg/errors" - "github.com/dgraph-io/badger/v2/y" - "github.com/dgraph-io/dgo/v200/protos/api" "github.com/dgraph-io/dgraph/ee/acl" "github.com/dgraph-io/dgraph/gql" @@ -305,16 +304,16 @@ func authorizeUser(ctx context.Context, userid string, password string) ( } // RefreshAcls queries for the ACL triples and refreshes the ACLs accordingly. -func RefreshAcls(closer *y.Closer) { - defer closer.Done() +func RefreshAcls(closer *z.Closer) { + defer func() { + glog.Infoln("RefreshAcls closed") + closer.Done() + }() if len(worker.Config.HmacSecret) == 0 { // the acl feature is not turned on return } - ticker := time.NewTicker(worker.Config.AclRefreshInterval) - defer ticker.Stop() - // retrieve the full data set of ACLs from the corresponding alpha server, and update the // aclCachePtr retrieveAcls := func() error { @@ -324,9 +323,7 @@ func RefreshAcls(closer *y.Closer) { ReadOnly: true, } - ctx := context.Background() - var err error - queryResp, err := (&Server{}).doQuery(ctx, &queryRequest, NoAuthorize) + queryResp, err := (&Server{}).doQuery(closer.Ctx(), &queryRequest, NoAuthorize) if err != nil { return errors.Errorf("unable to retrieve acls: %v", err) } @@ -340,14 +337,16 @@ func RefreshAcls(closer *y.Closer) { return nil } + ticker := time.NewTicker(worker.Config.AclRefreshInterval) + defer ticker.Stop() for { select { - case <-closer.HasBeenClosed(): - return case <-ticker.C: if err := retrieveAcls(); err != nil { - glog.Errorf("Error while retrieving acls:%v", err) + glog.Errorf("Error while retrieving acls: %v", err) } + case <-closer.HasBeenClosed(): + return } } } @@ -368,7 +367,12 @@ const queryAcls = ` ` // ResetAcl clears the aclCachePtr and upserts the Groot account. -func ResetAcl() { +func ResetAcl(closer *z.Closer) { + defer func() { + glog.Infof("ResetAcl closed") + closer.Done() + }() + if len(worker.Config.HmacSecret) == 0 { // The acl feature is not turned on. return @@ -435,8 +439,8 @@ func ResetAcl() { return nil } - for { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + for closer.Ctx().Err() == nil { + ctx, cancel := context.WithTimeout(closer.Ctx(), time.Minute) defer cancel() if err := upsertGuardians(ctx); err != nil { glog.Infof("Unable to upsert the guardian group. Error: %v", err) @@ -446,8 +450,8 @@ func ResetAcl() { break } - for { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + for closer.Ctx().Err() == nil { + ctx, cancel := context.WithTimeout(closer.Ctx(), time.Minute) defer cancel() if err := upsertGroot(ctx); err != nil { glog.Infof("Unable to upsert the groot account. Error: %v", err) @@ -583,7 +587,10 @@ func parsePredsFromMutation(nquads []*api.NQuad) []string { // use a map to dedup predicates predsMap := make(map[string]struct{}) for _, nquad := range nquads { - predsMap[nquad.Predicate] = struct{}{} + // _STAR_ALL is not a predicate in itself. + if nquad.Predicate != "_STAR_ALL" { + predsMap[nquad.Predicate] = struct{}{} + } } preds := make([]string, 0, len(predsMap)) @@ -658,7 +665,7 @@ func authorizeMutation(ctx context.Context, gmu *gql.Mutation) error { return nil } - blockedPreds, _ := authorizePreds(userId, groupIds, preds, acl.Write) + blockedPreds, allowedPreds := authorizePreds(userId, groupIds, preds, acl.Write) if len(blockedPreds) > 0 { var msg strings.Builder for key := range blockedPreds { @@ -668,7 +675,7 @@ func authorizeMutation(ctx context.Context, gmu *gql.Mutation) error { return status.Errorf(codes.PermissionDenied, "unauthorized to mutate following predicates: %s\n", msg.String()) } - + gmu.AllowedPreds = allowedPreds return nil } @@ -721,8 +728,10 @@ func parsePredsFromQuery(gqls []*gql.GraphQuery) predsAndvars { } preds := make([]string, 0, len(predsMap)) for pred := range predsMap { - if _, found := varsMap[pred]; !found { - preds = append(preds, pred) + if len(pred) > 0 { + if _, found := varsMap[pred]; !found { + preds = append(preds, pred) + } } } diff --git a/edgraph/server.go b/edgraph/server.go index 10dedc4533b..0394ae5de4f 100644 --- a/edgraph/server.go +++ b/edgraph/server.go @@ -58,6 +58,7 @@ 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 ( @@ -328,8 +329,8 @@ func (s *Server) Alter(ctx context.Context, op *api.Operation) (*api.Payload, er // reset their in-memory GraphQL schema _, err = UpdateGQLSchema(ctx, "", "") // recreate the admin account after a drop all operation - ResetAcl() - ResetCors() + ResetAcl(nil) + ResetCors(nil) return empty, err } @@ -353,8 +354,8 @@ func (s *Server) Alter(ctx context.Context, op *api.Operation) (*api.Payload, er // just reinsert the GraphQL schema, no need to alter dgraph schema as this was drop_data _, err = UpdateGQLSchema(ctx, graphQLSchema, "") // recreate the admin account after a drop data operation - ResetAcl() - ResetCors() + ResetAcl(nil) + ResetCors(nil) return empty, err } @@ -494,6 +495,9 @@ func (s *Server) doMutate(ctx context.Context, qc *queryContext, resp *api.Respo if x.WorkerConfig.LudicrousMode { // Mutations are automatically committed in case of ludicrous mode, so we don't // need to manually commit. + if resp.Txn == nil { + return errors.Wrapf(err, "Txn Context is nil") + } resp.Txn.Keys = resp.Txn.Keys[:0] resp.Txn.CommitTs = qc.req.StartTs return err @@ -1545,7 +1549,11 @@ func isDropAll(op *api.Operation) bool { // ResetCors make the dgraph to accept all the origins if no origins were given // by the users. -func ResetCors() { +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)) @@ -1565,8 +1573,8 @@ func ResetCors() { CommitNow: true, } - for { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + 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) @@ -1627,9 +1635,8 @@ func GetCorsOrigins(ctx context.Context) ([]string, error) { if err = json.Unmarshal(res.Json, corsRes); err != nil { return nil, err } - if len(corsRes.Me) > 1 { - glog.Errorf("Something went wrong in cors predicate, expected 1 predicate but got %d", - len(corsRes.Me)) + 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 050a093924e..61c88c3764d 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -1255,6 +1255,128 @@ func TestExpandQueryWithACLPermissions(t *testing.T) { testutil.CompareJSON(t, `{"me":[{"name":"RandomGuy","age":23, "nickname":"RG"},{"name":"RandomGuy2","age":25, "nickname":"RG2"}]}`, string(resp.GetJson())) +} +func TestDeleteQueryWithACLPermissions(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second) + defer cancel() + dg, err := testutil.DgraphClientWithGroot(testutil.SockAddr) + require.NoError(t, err) + + testutil.DropAll(t, dg) + + op := api.Operation{Schema: ` + name : string @index(exact) . + nickname : string @index(exact) . + age : int . + type Person { + name: string + nickname: string + age: int + } + `} + require.NoError(t, dg.Alter(ctx, &op)) + + resetUser(t) + + accessJwt, _, err := testutil.HttpLogin(&testutil.LoginParams{ + Endpoint: adminEndpoint, + UserID: "groot", + Passwd: "password", + }) + require.NoError(t, err, "login failed") + + createGroup(t, accessJwt, devGroup) + + addToGroup(t, accessJwt, userid, devGroup) + + txn := dg.NewTxn() + mutation := &api.Mutation{ + SetNquads: []byte(` + _:a "RandomGuy" . + _:a "23" . + _:a "RG" . + _:a "Person" . + _:b "RandomGuy2" . + _:b "25" . + _:b "RG2" . + _:b "Person" . + `), + CommitNow: true, + } + resp, err := txn.Mutate(ctx, mutation) + require.NoError(t, err) + + nodeUID := resp.Uids["a"] + query := `{q1(func: type(Person)){ + expand(_all_) + }}` + + // Test that groot has access to all the predicates + resp, err = dg.NewReadOnlyTxn().Query(ctx, query) + require.NoError(t, err, "Error while querying data") + testutil.CompareJSON(t, `{"q1":[{"name":"RandomGuy","age":23, "nickname": "RG"},{"name":"RandomGuy2","age":25, "nickname": "RG2"}]}`, + string(resp.GetJson())) + + // Give Write Access to alice for name and age predicate + addRulesToGroup(t, accessJwt, devGroup, []rule{{"name", Write.Code}, {"age", Write.Code}}) + + userClient, err := testutil.DgraphClient(testutil.SockAddr) + require.NoError(t, err) + time.Sleep(6 * time.Second) + + err = userClient.Login(ctx, userid, userpassword) + require.NoError(t, err) + + // delete S * * (user now has permission to name and age) + txn = userClient.NewTxn() + mutString := fmt.Sprintf("<%s> * * .", nodeUID) + mutation = &api.Mutation{ + DelNquads: []byte(mutString), + CommitNow: true, + } + _, err = txn.Mutate(ctx, mutation) + require.NoError(t, err) + + accessJwt, _, err = testutil.HttpLogin(&testutil.LoginParams{ + Endpoint: adminEndpoint, + UserID: "groot", + Passwd: "password", + }) + require.NoError(t, err, "login failed") + + resp, err = dg.NewReadOnlyTxn().Query(ctx, query) + require.NoError(t, err, "Error while querying data") + // Only name and age predicates got deleted via user - alice + testutil.CompareJSON(t, `{"q1":[{"nickname": "RG"},{"name":"RandomGuy2","age":25, "nickname": "RG2"}]}`, + string(resp.GetJson())) + + // Give write access of to dev + addRulesToGroup(t, accessJwt, devGroup, []rule{{"name", Write.Code}, {"age", Write.Code}, {"dgraph.type", Write.Code}}) + time.Sleep(6 * time.Second) + + // delete S * * (user now has permission to name, age and dgraph.type) + txn = userClient.NewTxn() + mutString = fmt.Sprintf("<%s> * * .", nodeUID) + mutation = &api.Mutation{ + DelNquads: []byte(mutString), + CommitNow: true, + } + _, err = txn.Mutate(ctx, mutation) + require.NoError(t, err) + + accessJwt, _, err = testutil.HttpLogin(&testutil.LoginParams{ + Endpoint: adminEndpoint, + UserID: "groot", + Passwd: "password", + }) + require.NoError(t, err, "login failed") + + resp, err = dg.NewReadOnlyTxn().Query(ctx, query) + require.NoError(t, err, "Error while querying data") + // Because alise had permission to dgraph.type the node reference has been deleted + testutil.CompareJSON(t, `{"q1":[{"name":"RandomGuy2","age":25, "nickname": "RG2"}]}`, + string(resp.GetJson())) + } func TestValQueryWithACLPermissions(t *testing.T) { @@ -1397,6 +1519,28 @@ func TestValQueryWithACLPermissions(t *testing.T) { `{"q1":[{"name":"RandomGuy","age":23},{"name":"RandomGuy2","age":25}], "q2":[{"name":"RandomGuy2","val(n)":"RandomGuy2","val(a)":25},{"name":"RandomGuy","val(n)":"RandomGuy","val(a)":23}]}`, }, + { + `{ + f as q1(func: has(name), orderasc: name) { + name + age + } + q2(func: uid(f), orderasc: name) { + name + age + } + }`, + "alice doesn't have access to name or age", + `{"q2":[]}`, + + `alice has access to name`, + `{"q1":[{"name":"RandomGuy"},{"name":"RandomGuy2"}], + "q2":[{"name":"RandomGuy"},{"name":"RandomGuy2"}]}`, + + "alice has access to name and age", + `{"q1":[{"name":"RandomGuy","age":23},{"name":"RandomGuy2","age":25}], + "q2":[{"name":"RandomGuy2","age":25},{"name":"RandomGuy","age":23}]}`, + }, } userClient, err := testutil.DgraphClient(testutil.SockAddr) diff --git a/go.mod b/go.mod index 0c2a6dbdca6..f8c611c5326 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,10 @@ require ( github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f // indirect github.com/blevesearch/snowballstem v0.0.0-20180110192139-26b06a2c243d // indirect github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd - github.com/dgraph-io/badger/v2 v2.2007.1 + github.com/dgraph-io/badger/v2 v2.2007.2-0.20200827131741-d5a25b83fbf4 github.com/dgraph-io/dgo/v200 v200.0.0-20200401175452-e463f9234453 github.com/dgraph-io/graphql-transport-ws v0.0.0-20200916064635-48589439591b - github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de + github.com/dgraph-io/ristretto v0.0.4-0.20200904131139-4dec2770af66 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 diff --git a/go.sum b/go.sum index a92826cdf96..83bce13e5ff 100644 --- a/go.sum +++ b/go.sum @@ -81,14 +81,15 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= -github.com/dgraph-io/badger/v2 v2.2007.1 h1:t36VcBCpo4SsmAD5M8wVv1ieVzcALyGfaJ92z4ccULM= -github.com/dgraph-io/badger/v2 v2.2007.1/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= +github.com/dgraph-io/badger/v2 v2.2007.2-0.20200827131741-d5a25b83fbf4 h1:DUDFTVgqZysKplH39/ya0aI4+zGm91L9QttXgITT2YE= +github.com/dgraph-io/badger/v2 v2.2007.2-0.20200827131741-d5a25b83fbf4/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= github.com/dgraph-io/dgo/v200 v200.0.0-20200401175452-e463f9234453 h1:DTgOrw91nMIukDm/WEvdobPLl0LgeDd/JE66+24jBks= github.com/dgraph-io/dgo/v200 v200.0.0-20200401175452-e463f9234453/go.mod h1:Co+FwJrnndSrPORO8Gdn20dR7FPTfmXr0W/su0Ve/Ig= github.com/dgraph-io/graphql-transport-ws v0.0.0-20200916064635-48589439591b h1:PDEhlwHpkEQ5WBfOOKZCNZTXFDGyCEWTYDhxGQbyIpk= github.com/dgraph-io/graphql-transport-ws v0.0.0-20200916064635-48589439591b/go.mod h1:7z3c/5w0sMYYZF5bHsrh8IH4fKwG5O5Y70cPH1ZLLRQ= -github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.0.4-0.20200904131139-4dec2770af66 h1:ectpJv2tGhTudyk0JhqE/53o/ObH30u5yt/yThsAn3I= +github.com/dgraph-io/ristretto v0.0.4-0.20200904131139-4dec2770af66/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU= diff --git a/gql/mutation.go b/gql/mutation.go index 2a2afb8e2f9..50438d0abae 100644 --- a/gql/mutation.go +++ b/gql/mutation.go @@ -32,9 +32,10 @@ var ( // Mutation stores the strings corresponding to set and delete operations. type Mutation struct { - Cond string - Set []*api.NQuad - Del []*api.NQuad + Cond string + Set []*api.NQuad + Del []*api.NQuad + AllowedPreds []string Metadata *pb.Metadata } diff --git a/graphql/admin/admin.go b/graphql/admin/admin.go index 4846a1bf3ce..d685aac775f 100644 --- a/graphql/admin/admin.go +++ b/graphql/admin/admin.go @@ -26,7 +26,6 @@ import ( "github.com/pkg/errors" badgerpb "github.com/dgraph-io/badger/v2/pb" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgraph/edgraph" "github.com/dgraph-io/dgraph/graphql/resolve" "github.com/dgraph-io/dgraph/graphql/schema" @@ -35,6 +34,7 @@ import ( "github.com/dgraph-io/dgraph/query" "github.com/dgraph-io/dgraph/worker" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" ) const ( @@ -415,7 +415,7 @@ type adminServer struct { // NewServers initializes the GraphQL servers. It sets up an empty server for the // main /graphql endpoint and an admin server. The result is mainServer, adminServer. -func NewServers(withIntrospection bool, globalEpoch *uint64, closer *y.Closer) (web.IServeGraphQL, +func NewServers(withIntrospection bool, globalEpoch *uint64, closer *z.Closer) (web.IServeGraphQL, web.IServeGraphQL, *GraphQLHealthStore) { gqlSchema, err := schema.FromString("") if err != nil { @@ -444,7 +444,7 @@ func newAdminResolver( fns *resolve.ResolverFns, withIntrospection bool, epoch *uint64, - closer *y.Closer) *resolve.RequestResolver { + closer *z.Closer) *resolve.RequestResolver { adminSchema, err := schema.FromString(graphqlAdminSchema) if err != nil { @@ -793,13 +793,18 @@ func (as *adminServer) resetSchema(gqlSchema schema.Schema) { // set status as updating schema mainHealthStore.updatingSchema() - resolverFactory := resolverFactoryWithErrorMsg(errResolverNotFound) - // it is nil after drop_all - if gqlSchema != nil { - resolverFactory = resolverFactory.WithConventionResolvers(gqlSchema, as.fns) - } - if as.withIntrospection { - resolverFactory.WithSchemaIntrospection() + var resolverFactory resolve.ResolverFactory + // If schema is nil (which becomes after drop_all) then do not attach Resolver for + // introspection operations, and set GQL schema to empty. + if gqlSchema == nil { + resolverFactory = resolverFactoryWithErrorMsg(errNoGraphQLSchema) + gqlSchema, _ = schema.FromString("") + } else { + resolverFactory = resolverFactoryWithErrorMsg(errResolverNotFound). + WithConventionResolvers(gqlSchema, as.fns) + if as.withIntrospection { + resolverFactory.WithSchemaIntrospection() + } } // Increment the Epoch when you get a new schema. So, that subscription's local epoch diff --git a/graphql/admin/restore_status.go b/graphql/admin/restore_status.go index 265645e9708..9f095f90c51 100644 --- a/graphql/admin/restore_status.go +++ b/graphql/admin/restore_status.go @@ -19,6 +19,7 @@ package admin import ( "context" "encoding/json" + "fmt" "github.com/dgraph-io/dgraph/graphql/resolve" "github.com/dgraph-io/dgraph/graphql/schema" @@ -40,9 +41,25 @@ func unknownStatus(q schema.Query, err error) *resolve.Resolved { } } +func getRestoreStatusInput(q schema.Query) (int64, error) { + restoreId := q.ArgValue("restoreId") + switch v := restoreId.(type) { + case int64: + return v, nil + case json.Number: + return v.Int64() + default: + return -1, fmt.Errorf("Invalid value of restoreId") + } + +} + func resolveRestoreStatus(ctx context.Context, q schema.Query) *resolve.Resolved { - restoreId := int(q.ArgValue("restoreId").(int64)) - status, err := worker.ProcessRestoreStatus(ctx, restoreId) + restoreId, err := getRestoreStatusInput(q) + if err != nil { + return unknownStatus(q, err) + } + status, err := worker.ProcessRestoreStatus(ctx, int(restoreId)) if err != nil { return unknownStatus(q, err) } diff --git a/graphql/admin/restore_status_test.go b/graphql/admin/restore_status_test.go new file mode 100644 index 00000000000..94d6fa5c1fa --- /dev/null +++ b/graphql/admin/restore_status_test.go @@ -0,0 +1,38 @@ +package admin + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/dgraph-io/dgraph/graphql/schema" + "github.com/dgraph-io/dgraph/graphql/test" + "github.com/stretchr/testify/require" +) + +func TestRestoreStatus(t *testing.T) { + gqlSchema := test.LoadSchema(t, graphqlAdminSchema) + Query := `query restoreStatus($restoreId: Int!) { + restoreStatus(restoreId: $restoreId) { + status + errors + } + }` + variables := `{"restoreId": 2 }` + vars := make(map[string]interface{}) + d := json.NewDecoder(strings.NewReader(variables)) + d.UseNumber() + err := d.Decode(&vars) + require.NoError(t, err) + + op, err := gqlSchema.Operation( + &schema.Request{ + Query: Query, + Variables: vars, + }) + require.NoError(t, err) + gqlQuery := test.GetQuery(t, op) + v, err := getRestoreStatusInput(gqlQuery) + require.NoError(t, err) + require.IsType(t, int64(2), v, nil) +} diff --git a/graphql/admin/schema.go b/graphql/admin/schema.go index 7e1c23448f6..4cdec329587 100644 --- a/graphql/admin/schema.go +++ b/graphql/admin/schema.go @@ -20,8 +20,8 @@ import ( "bytes" "context" "encoding/json" - dgoapi "github.com/dgraph-io/dgo/v200/protos/api" + "github.com/dgraph-io/dgraph/edgraph" "github.com/dgraph-io/dgraph/gql" "github.com/dgraph-io/dgraph/graphql/resolve" diff --git a/graphql/admin/shutdown.go b/graphql/admin/shutdown.go index 5fb6bf63344..e3d3469c16d 100644 --- a/graphql/admin/shutdown.go +++ b/graphql/admin/shutdown.go @@ -19,16 +19,16 @@ package admin import ( "context" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgraph/graphql/resolve" "github.com/dgraph-io/dgraph/graphql/schema" + "github.com/dgraph-io/ristretto/z" "github.com/golang/glog" ) var ( // ServerCloser is used to signal and wait for other goroutines to return gracefully after user // requests shutdown. - ServerCloser *y.Closer + ServerCloser *z.Closer ) func resolveShutdown(ctx context.Context, m schema.Mutation) (*resolve.Resolved, bool) { diff --git a/graphql/e2e/auth/add_mutation_test.go b/graphql/e2e/auth/add_mutation_test.go index b89ac9d6cde..dcdc94d778c 100644 --- a/graphql/e2e/auth/add_mutation_test.go +++ b/graphql/e2e/auth/add_mutation_test.go @@ -26,22 +26,6 @@ import ( "github.com/stretchr/testify/require" ) -func (us *UserSecret) delete(t *testing.T, user, role string) { - getParams := &common.GraphQLParams{ - Headers: getJWT(t, user, role), - Query: ` - mutation deleteUserSecret($ids: [ID!]) { - deleteUserSecret(filter:{id:$ids}) { - msg - } - } - `, - Variables: map[string]interface{}{"ids": []string{us.Id}}, - } - gqlResponse := getParams.ExecuteAsPost(t, graphqlURL) - require.Nil(t, gqlResponse.Errors) -} - func (p *Project) delete(t *testing.T, user, role string) { getParams := &common.GraphQLParams{ Headers: getJWT(t, user, role), @@ -146,7 +130,7 @@ func TestAddDeepFilter(t *testing.T) { Name: "project_add_2", Roles: []*Role{{ Permission: "ADMIN", - AssignedTo: []*User{{ + AssignedTo: []*common.User{{ Username: "user2", }}, }}, @@ -162,12 +146,12 @@ func TestAddDeepFilter(t *testing.T) { Name: "project_add_4", Roles: []*Role{{ Permission: "ADMIN", - AssignedTo: []*User{{ + AssignedTo: []*common.User{{ Username: "user6", }}, }, { Permission: "VIEW", - AssignedTo: []*User{{ + AssignedTo: []*common.User{{ Username: "user6", }}, }}, @@ -212,7 +196,7 @@ func TestAddDeepFilter(t *testing.T) { err := json.Unmarshal([]byte(tcase.result), &expected) require.NoError(t, err) - err = json.Unmarshal([]byte(gqlResponse.Data), &result) + err = json.Unmarshal(gqlResponse.Data, &result) require.NoError(t, err) opt := cmpopts.IgnoreFields(Column{}, "ColID") @@ -249,7 +233,7 @@ func TestAddOrRBACFilter(t *testing.T) { Name: "project_add_2", Roles: []*Role{{ Permission: "ADMIN", - AssignedTo: []*User{{ + AssignedTo: []*common.User{{ Username: "user2", }}, }}, @@ -262,12 +246,12 @@ func TestAddOrRBACFilter(t *testing.T) { Name: "project_add_3", Roles: []*Role{{ Permission: "ADMIN", - AssignedTo: []*User{{ + AssignedTo: []*common.User{{ Username: "user7", }}, }, { Permission: "VIEW", - AssignedTo: []*User{{ + AssignedTo: []*common.User{{ Username: "user7", }}, }}, @@ -308,7 +292,7 @@ func TestAddOrRBACFilter(t *testing.T) { err := json.Unmarshal([]byte(tcase.result), &expected) require.NoError(t, err) - err = json.Unmarshal([]byte(gqlResponse.Data), &result) + err = json.Unmarshal(gqlResponse.Data, &result) require.NoError(t, err) opt := cmpopts.IgnoreFields(Project{}, "ProjID") @@ -329,13 +313,13 @@ func TestAddAndRBACFilterMultiple(t *testing.T) { result: `{"addIssue": {"issue":[{"msg":"issue_add_5"}, {"msg":"issue_add_6"}, {"msg":"issue_add_7"}]}}`, variables: map[string]interface{}{"issues": []*Issue{{ Msg: "issue_add_5", - Owner: &User{Username: "user8"}, + Owner: &common.User{Username: "user8"}, }, { Msg: "issue_add_6", - Owner: &User{Username: "user8"}, + Owner: &common.User{Username: "user8"}, }, { Msg: "issue_add_7", - Owner: &User{Username: "user8"}, + Owner: &common.User{Username: "user8"}, }}}, }, { user: "user8", @@ -343,13 +327,13 @@ func TestAddAndRBACFilterMultiple(t *testing.T) { result: ``, variables: map[string]interface{}{"issues": []*Issue{{ Msg: "issue_add_8", - Owner: &User{Username: "user8"}, + Owner: &common.User{Username: "user8"}, }, { Msg: "issue_add_9", - Owner: &User{Username: "user8"}, + Owner: &common.User{Username: "user8"}, }, { Msg: "issue_add_10", - Owner: &User{Username: "user9"}, + Owner: &common.User{Username: "user9"}, }}}, }} @@ -386,7 +370,7 @@ func TestAddAndRBACFilterMultiple(t *testing.T) { err := json.Unmarshal([]byte(tcase.result), &expected) require.NoError(t, err) - err = json.Unmarshal([]byte(gqlResponse.Data), &result) + err = json.Unmarshal(gqlResponse.Data, &result) require.NoError(t, err) opt := cmpopts.IgnoreFields(Issue{}, "Id") @@ -407,7 +391,7 @@ func TestAddAndRBACFilter(t *testing.T) { result: `{"addIssue": {"issue":[{"msg":"issue_add_1"}]}}`, variables: map[string]interface{}{"issue": &Issue{ Msg: "issue_add_1", - Owner: &User{Username: "user7"}, + Owner: &common.User{Username: "user7"}, }}, }, { user: "user7", @@ -415,7 +399,7 @@ func TestAddAndRBACFilter(t *testing.T) { result: ``, variables: map[string]interface{}{"issue": &Issue{ Msg: "issue_add_2", - Owner: &User{Username: "user8"}, + Owner: &common.User{Username: "user8"}, }}, }, { user: "user7", @@ -423,7 +407,7 @@ func TestAddAndRBACFilter(t *testing.T) { result: ``, variables: map[string]interface{}{"issue": &Issue{ Msg: "issue_add_3", - Owner: &User{Username: "user7"}, + Owner: &common.User{Username: "user7"}, }}, }} @@ -460,7 +444,7 @@ func TestAddAndRBACFilter(t *testing.T) { err := json.Unmarshal([]byte(tcase.result), &expected) require.NoError(t, err) - err = json.Unmarshal([]byte(gqlResponse.Data), &result) + err = json.Unmarshal(gqlResponse.Data, &result) require.NoError(t, err) opt := cmpopts.IgnoreFields(Issue{}, "Id") @@ -522,7 +506,7 @@ func TestAddComplexFilter(t *testing.T) { RegionsAvailable: []*Region{{ Name: "add_region_2", Global: false, - Users: []*User{{ + Users: []*common.User{{ Username: "user8", }}, }}, @@ -563,7 +547,7 @@ func TestAddComplexFilter(t *testing.T) { err := json.Unmarshal([]byte(tcase.result), &expected) require.NoError(t, err) - err = json.Unmarshal([]byte(gqlResponse.Data), &result) + err = json.Unmarshal(gqlResponse.Data, &result) require.NoError(t, err) opt := cmpopts.IgnoreFields(Movie{}, "Id") @@ -628,7 +612,7 @@ func TestAddRBACFilter(t *testing.T) { err := json.Unmarshal([]byte(tcase.result), &expected) require.NoError(t, err) - err = json.Unmarshal([]byte(gqlResponse.Data), &result) + err = json.Unmarshal(gqlResponse.Data, &result) require.NoError(t, err) opt := cmpopts.IgnoreFields(Log{}, "Id") @@ -646,14 +630,14 @@ func TestAddGQLOnly(t *testing.T) { testCases := []TestCase{{ user: "user1", result: `{"addUserSecret":{"usersecret":[{"aSecret":"secret1"}]}}`, - variables: map[string]interface{}{"user": &UserSecret{ + variables: map[string]interface{}{"user": &common.UserSecret{ ASecret: "secret1", OwnedBy: "user1", }}, }, { user: "user2", result: ``, - variables: map[string]interface{}{"user": &UserSecret{ + variables: map[string]interface{}{"user": &common.UserSecret{ ASecret: "secret2", OwnedBy: "user1", }}, @@ -670,7 +654,7 @@ func TestAddGQLOnly(t *testing.T) { ` var expected, result struct { AddUserSecret struct { - UserSecret []*UserSecret + UserSecret []*common.UserSecret } } @@ -694,13 +678,13 @@ func TestAddGQLOnly(t *testing.T) { err = json.Unmarshal([]byte(gqlResponse.Data), &result) require.NoError(t, err) - opt := cmpopts.IgnoreFields(UserSecret{}, "Id") + opt := cmpopts.IgnoreFields(common.UserSecret{}, "Id") if diff := cmp.Diff(expected, result, opt); diff != "" { t.Errorf("result mismatch (-want +got):\n%s", diff) } for _, i := range result.AddUserSecret.UserSecret { - i.delete(t, tcase.user, tcase.role) + i.Delete(t, tcase.user, tcase.role, metaInfo) } } } diff --git a/graphql/e2e/auth/auth_test.go b/graphql/e2e/auth/auth_test.go index cc21dcde262..9f85bc35149 100644 --- a/graphql/e2e/auth/auth_test.go +++ b/graphql/e2e/auth/auth_test.go @@ -43,24 +43,11 @@ var ( metaInfo *testutil.AuthMeta ) -type User struct { - Username string `json:"username,omitempty"` - Age uint64 `json:"age,omitempty"` - IsPublic bool `json:"isPublic,omitempty"` - Disabled bool `json:"disabled,omitempty"` -} - -type UserSecret struct { - Id string `json:"id,omitempty"` - ASecret string `json:"aSecret,omitempty"` - OwnedBy string `json:"ownedBy,omitempty"` -} - type Region struct { - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Users []*User `json:"users,omitempty"` - Global bool `json:"global,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Users []*common.User `json:"users,omitempty"` + Global bool `json:"global,omitempty"` } type Movie struct { @@ -71,9 +58,9 @@ type Movie struct { } type Issue struct { - Id string `json:"id,omitempty"` - Msg string `json:"msg,omitempty"` - Owner *User `json:"owner,omitempty"` + Id string `json:"id,omitempty"` + Msg string `json:"msg,omitempty"` + Owner *common.User `json:"owner,omitempty"` } type Log struct { @@ -89,16 +76,16 @@ type ComplexLog struct { } type Role struct { - Id string `json:"id,omitempty"` - Permission string `json:"permission,omitempty"` - AssignedTo []*User `json:"assignedTo,omitempty"` + Id string `json:"id,omitempty"` + Permission string `json:"permission,omitempty"` + AssignedTo []*common.User `json:"assignedTo,omitempty"` } type Ticket struct { - Id string `json:"id,omitempty"` - OnColumn *Column `json:"onColumn,omitempty"` - Title string `json:"title,omitempty"` - AssignedTo []*User `json:"assignedTo,omitempty"` + Id string `json:"id,omitempty"` + OnColumn *Column `json:"onColumn,omitempty"` + Title string `json:"title,omitempty"` + AssignedTo []*common.User `json:"assignedTo,omitempty"` } type Column struct { @@ -120,6 +107,18 @@ type Student struct { Email string `json:"email,omitempty"` } +type Task struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Occurrences []*TaskOccurrence `json:"occurrences,omitempty"` +} + +type TaskOccurrence struct { + Id string `json:"id,omitempty"` + Due string `json:"due,omitempty"` + Comp string `json:"comp,omitempty"` +} + type TestCase struct { user string role string @@ -136,6 +135,23 @@ type uidResult struct { } } +type Tasks []Task + +func (tasks Tasks) add(t *testing.T) { + getParams := &common.GraphQLParams{ + Query: ` + mutation AddTask($tasks : [AddTaskInput!]!) { + addTask(input: $tasks) { + numUids + } + } + `, + Variables: map[string]interface{}{"tasks": tasks}, + } + gqlResponse := getParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) +} + func (r *Region) add(t *testing.T, user, role string) { getParams := &common.GraphQLParams{ Headers: getJWT(t, user, role), @@ -270,6 +286,42 @@ func (s Student) add(t *testing.T) { require.JSONEq(t, result, string(gqlResponse.Data)) } +func TestAddMutationWithXid(t *testing.T) { + mutation := ` + mutation addTweets($tweet: AddTweetsInput!){ + addTweets(input: [$tweet]) { + numUids + } + } + ` + + tweet := common.Tweets{ + Id: "tweet1", + Text: "abc", + Timestamp: "2020-10-10", + } + user := "foo" + addTweetsParams := &common.GraphQLParams{ + Headers: common.GetJWT(t, user, "", metaInfo), + Query: mutation, + Variables: map[string]interface{}{"tweet": tweet}, + } + + // Add the tweet for the first time. + gqlResponse := addTweetsParams.ExecuteAsPost(t, common.GraphqlURL) + require.Nil(t, gqlResponse.Errors) + + // Re-adding the tweet should fail. + gqlResponse = addTweetsParams.ExecuteAsPost(t, common.GraphqlURL) + require.Error(t, gqlResponse.Errors) + require.Equal(t, len(gqlResponse.Errors), 1) + require.Contains(t, gqlResponse.Errors[0].Error(), + "GraphQL debug: id already exists for type Tweets") + + // Clear the tweet. + tweet.DeleteByID(t, user, metaInfo) +} + func TestAuthWithDgraphDirective(t *testing.T) { students := []Student{ { @@ -401,6 +453,141 @@ func TestAuthRulesWithMissingJWT(t *testing.T) { } } +func TestOrderAndOffset(t *testing.T) { + tasks := Tasks{ + Task{ + Name: "First Task four occurrence", + Occurrences: []*TaskOccurrence{ + {Due: "2020-07-19T08:00:00", Comp: "2020-07-19T08:00:00"}, + {Due: "2020-07-19T08:00:00", Comp: "2020-07-19T08:00:00"}, + {Due: "2020-07-19T08:00:00", Comp: "2020-07-19T08:00:00"}, + {Due: "2020-07-19T08:00:00", Comp: "2020-07-19T08:00:00"}, + }, + }, + Task{ + Name: "Second Task single occurrence", + Occurrences: []*TaskOccurrence{ + {Due: "2020-07-19T08:00:00", Comp: "2020-07-19T08:00:00"}, + }, + }, + Task{ + Name: "Third Task no occurrence", + Occurrences: []*TaskOccurrence{}, + }, + Task{ + Name: "Fourth Task two occurrences", + Occurrences: []*TaskOccurrence{ + {Due: "2020-07-19T08:00:00", Comp: "2020-07-19T08:00:00"}, + {Due: "2020-07-19T08:00:00", Comp: "2020-07-19T08:00:00"}, + }, + }, + } + tasks.add(t) + + query := ` + query { + queryTask(first: 4, order: {asc : name}) { + name + occurrences(first: 2) { + due + comp + } + } + } + ` + testCases := []TestCase{{ + user: "user1", + role: "ADMIN", + result: ` + { + "queryTask": [ + { + "name": "First Task four occurrence", + "occurrences": [ + { + "due": "2020-07-19T08:00:00Z", + "comp": "2020-07-19T08:00:00Z" + }, + { + "due": "2020-07-19T08:00:00Z", + "comp": "2020-07-19T08:00:00Z" + } + ] + }, + { + "name": "Fourth Task two occurrences", + "occurrences": [ + { + "due": "2020-07-19T08:00:00Z", + "comp": "2020-07-19T08:00:00Z" + }, + { + "due": "2020-07-19T08:00:00Z", + "comp": "2020-07-19T08:00:00Z" + } + ] + }, + { + "name": "Second Task single occurrence", + "occurrences": [ + { + "due": "2020-07-19T08:00:00Z", + "comp": "2020-07-19T08:00:00Z" + } + ] + }, + { + "name": "Third Task no occurrence", + "occurrences": [] + } + ] + } + `, + }} + + for _, tcase := range testCases { + t.Run(tcase.role+tcase.user, func(t *testing.T) { + getUserParams := &common.GraphQLParams{ + Headers: getJWT(t, tcase.user, tcase.role), + Query: query, + } + + gqlResponse := getUserParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) + + require.JSONEq(t, string(gqlResponse.Data), tcase.result) + }) + } + + // Clean up `Task` + getParams := &common.GraphQLParams{ + Query: ` + mutation DelTask { + deleteTask(filter: {}) { + numUids + } + } + `, + Variables: map[string]interface{}{"tasks": tasks}, + } + gqlResponse := getParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) + + // Clean up `TaskOccurrence` + getParams = &common.GraphQLParams{ + Query: ` + mutation DelTaskOccuerence { + deleteTaskOccurrence(filter: {}) { + numUids + } + } + `, + Variables: map[string]interface{}{"tasks": tasks}, + } + gqlResponse = getParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) +} + func TestOrRBACFilter(t *testing.T) { testCases := []TestCase{{ user: "user1", @@ -1073,6 +1260,67 @@ func TestDeleteDeepAuthRule(t *testing.T) { } } +func TestDeepRBACValueCascade(t *testing.T) { + testCases := []TestCase{ + { + user: "user1", + role: "USER", + query: ` + query { + queryUser (filter:{username:{eq:"user1"}}) @cascade { + username + issues { + msg + } + } + }`, + result: `{"queryUser": []}`, + }, + { + user: "user1", + role: "USER", + query: ` + query { + queryUser (filter:{username:{eq:"user1"}}) { + username + issues @cascade { + msg + } + } + }`, + result: `{"queryUser": [{"username": "user1", "issues":[]}]}`, + }, + { + user: "user1", + role: "ADMIN", + query: ` + query { + queryUser (filter:{username:{eq:"user1"}}) @cascade { + username + issues { + msg + } + } + }`, + result: `{"queryUser":[{"username":"user1","issues":[{"msg":"Issue1"}]}]}`, + }, + } + + for _, tcase := range testCases { + t.Run(tcase.role+tcase.user, func(t *testing.T) { + getUserParams := &common.GraphQLParams{ + Headers: common.GetJWT(t, tcase.user, tcase.role, metaInfo), + Query: tcase.query, + } + + gqlResponse := getUserParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) + + require.JSONEq(t, string(gqlResponse.Data), tcase.result) + }) + } +} + func TestMain(m *testing.M) { schemaFile := "schema.graphql" schema, err := ioutil.ReadFile(schemaFile) @@ -1099,10 +1347,11 @@ func TestMain(m *testing.M) { } metaInfo = &testutil.AuthMeta{ - PublicKey: authMeta.VerificationKey, - Namespace: authMeta.Namespace, - Algo: authMeta.Algo, - Header: authMeta.Header, + PublicKey: authMeta.VerificationKey, + Namespace: authMeta.Namespace, + Algo: authMeta.Algo, + Header: authMeta.Header, + PrivateKeyPath: "./sample_private_key.pem", } common.BootstrapServer(authSchema, data) diff --git a/graphql/e2e/auth/debug_off/debug_off_test.go b/graphql/e2e/auth/debug_off/debug_off_test.go new file mode 100644 index 00000000000..3f6a3df5012 --- /dev/null +++ b/graphql/e2e/auth/debug_off/debug_off_test.go @@ -0,0 +1,169 @@ +package debugoff + +import ( + "encoding/json" + "io/ioutil" + "os" + "testing" + + "github.com/dgraph-io/dgraph/graphql/authorization" + "github.com/dgraph-io/dgraph/graphql/e2e/common" + "github.com/dgraph-io/dgraph/testutil" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +var ( + metaInfo *testutil.AuthMeta +) + +type TestCase struct { + user string + role string + result string + name string + variables map[string]interface{} +} + +func TestAddGQL(t *testing.T) { + testCases := []TestCase{{ + user: "user1", + result: `{"addUserSecret":{"usersecret":[{"aSecret":"secret1"}]}}`, + variables: map[string]interface{}{"user": &common.UserSecret{ + ASecret: "secret1", + OwnedBy: "user1", + }}, + }, { + user: "user2", + result: ``, + variables: map[string]interface{}{"user": &common.UserSecret{ + ASecret: "secret2", + OwnedBy: "user1", + }}, + }} + + query := ` + mutation addUser($user: AddUserSecretInput!) { + addUserSecret(input: [$user]) { + userSecret { + aSecret + } + } + } + ` + var expected, result struct { + AddUserSecret struct { + UserSecret []*common.UserSecret + } + } + + for _, tcase := range testCases { + getUserParams := &common.GraphQLParams{ + Headers: common.GetJWT(t, tcase.user, tcase.role, metaInfo), + Query: query, + Variables: tcase.variables, + } + gqlResponse := getUserParams.ExecuteAsPost(t, common.GraphqlURL) + if tcase.result == "" { + require.Equal(t, len(gqlResponse.Errors), 0) + continue + } + + require.Nil(t, gqlResponse.Errors) + + err := json.Unmarshal([]byte(tcase.result), &expected) + require.NoError(t, err) + err = json.Unmarshal([]byte(gqlResponse.Data), &result) + require.NoError(t, err) + + opt := cmpopts.IgnoreFields(common.UserSecret{}, "Id") + if diff := cmp.Diff(expected, result, opt); diff != "" { + t.Errorf("result mismatch (-want +got):\n%s", diff) + } + + for _, i := range result.AddUserSecret.UserSecret { + i.Delete(t, tcase.user, tcase.role, metaInfo) + } + } +} + +func TestAddMutationWithXid(t *testing.T) { + mutation := ` + mutation addTweets($tweet: AddTweetsInput!){ + addTweets(input: [$tweet]) { + numUids + } + } + ` + + tweet := common.Tweets{ + Id: "tweet1", + Text: "abc", + Timestamp: "2020-10-10", + } + user := "foo" + addTweetsParams := &common.GraphQLParams{ + Headers: common.GetJWT(t, user, "", metaInfo), + Query: mutation, + Variables: map[string]interface{}{"tweet": tweet}, + } + + // Add the tweet for the first time. + gqlResponse := addTweetsParams.ExecuteAsPost(t, common.GraphqlURL) + require.Nil(t, gqlResponse.Errors) + + // Re-adding the tweet should fail. + gqlResponse = addTweetsParams.ExecuteAsPost(t, common.GraphqlURL) + require.Nil(t, gqlResponse.Errors) + + // Clear the tweet. + tweet.DeleteByID(t, user, metaInfo) +} + +func TestMain(m *testing.M) { + schemaFile := "../schema.graphql" + schema, err := ioutil.ReadFile(schemaFile) + if err != nil { + panic(err) + } + + jsonFile := "../test_data.json" + data, err := ioutil.ReadFile(jsonFile) + if err != nil { + panic(errors.Wrapf(err, "Unable to read file %s.", jsonFile)) + } + + jwtAlgo := []string{authorization.HMAC256, authorization.RSA256} + for _, algo := range jwtAlgo { + authSchema, err := testutil.AppendAuthInfo(schema, algo, "../sample_public_key.pem") + if err != nil { + panic(err) + } + + authMeta, err := authorization.Parse(string(authSchema)) + if err != nil { + panic(err) + } + + metaInfo = &testutil.AuthMeta{ + PublicKey: authMeta.VerificationKey, + Namespace: authMeta.Namespace, + Algo: authMeta.Algo, + Header: authMeta.Header, + PrivateKeyPath: "../sample_private_key.pem", + } + + common.BootstrapServer(authSchema, data) + // Data is added only in the first iteration, but the schema is added every iteration. + if data != nil { + data = nil + } + exitCode := m.Run() + if exitCode != 0 { + os.Exit(exitCode) + } + } + os.Exit(0) +} diff --git a/graphql/e2e/auth/debug_off/docker-compose.yml b/graphql/e2e/auth/debug_off/docker-compose.yml new file mode 100644 index 00000000000..c5b064e9725 --- /dev/null +++ b/graphql/e2e/auth/debug_off/docker-compose.yml @@ -0,0 +1,69 @@ +version: "3.5" +services: + zero: + image: dgraph/dgraph:latest + container_name: zero1 + working_dir: /data/zero1 + ports: + - 5180:5180 + - 6180:6180 + labels: + cluster: test + service: zero1 + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + command: /gobin/dgraph zero -o 100 --logtostderr -v=2 --bindall --expose_trace --profile_mode block --block_rate 10 --my=zero1:5180 + + alpha: + image: dgraph/dgraph:latest + container_name: alpha1 + working_dir: /data/alpha1 + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + ports: + - 8180:8180 + - 9180:9180 + labels: + cluster: test + service: alpha1 + command: /gobin/dgraph alpha --lru_mb=1024 --zero=zero1:5180 -o 100 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=3 --whitelist 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 --my=alpha1:7180 + + zeroAdmin: + image: dgraph/dgraph:latest + container_name: zeroAdmin + working_dir: /data/zeroAdmin + ports: + - 5280:5280 + - 6280:6280 + labels: + cluster: admintest + service: zeroAdmin + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + command: /gobin/dgraph zero -o 200 --logtostderr -v=2 --bindall --expose_trace --profile_mode block --block_rate 10 --my=zeroAdmin:5280 + + alphaAdmin: + image: dgraph/dgraph:latest + container_name: alphaAdmin + working_dir: /data/alphaAdmin + volumes: + - type: bind + source: $GOPATH/bin + target: /gobin + read_only: true + ports: + - 8280:8280 + - 9280:9280 + labels: + cluster: admintest + service: alphaAdmin + command: /gobin/dgraph alpha --lru_mb=1024 --zero=zeroAdmin:5280 -o 200 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --whitelist 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 --my=alphaAdmin:7280 \ No newline at end of file diff --git a/graphql/e2e/auth/delete_mutation_test.go b/graphql/e2e/auth/delete_mutation_test.go index 20e8562523f..e48e12518f9 100644 --- a/graphql/e2e/auth/delete_mutation_test.go +++ b/graphql/e2e/auth/delete_mutation_test.go @@ -361,3 +361,69 @@ func TestDeleteNestedFilter(t *testing.T) { }) } } + +func TestDeleteRBACRuleInverseField(t *testing.T) { + mutation := ` + mutation addTweets($tweet: AddTweetsInput!){ + addTweets(input: [$tweet]) { + numUids + } + } + ` + + addTweetsParams := &common.GraphQLParams{ + Headers: getJWT(t, "foo", ""), + Query: mutation, + Variables: map[string]interface{}{"tweet": common.Tweets{ + Id: "tweet1", + Text: "abc", + Timestamp: "2020-10-10", + User: &common.User{ + Username: "foo", + }, + }}, + } + + gqlResponse := addTweetsParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) + + testCases := []TestCase{ + { + user: "foobar", + role: "admin", + result: `{"deleteTweets":{"numUids":0,"tweets":[]}}`, + }, + { + user: "foo", + role: "admin", + result: `{"deleteTweets":{"numUids":1,"tweets":[ {"text": "abc"}]}}`, + }, + } + + mutation = ` + mutation { + deleteTweets( + filter: { + text: {anyoftext: "abc"} + }) { + numUids + tweets { + text + } + } + } + ` + + for _, tcase := range testCases { + t.Run(tcase.role+tcase.user, func(t *testing.T) { + deleteTweetsParams := &common.GraphQLParams{ + Headers: getJWT(t, tcase.user, tcase.role), + Query: mutation, + } + + gqlResponse := deleteTweetsParams.ExecuteAsPost(t, graphqlURL) + require.Nil(t, gqlResponse.Errors) + require.JSONEq(t, string(gqlResponse.Data), tcase.result) + }) + } +} diff --git a/graphql/e2e/auth/docker-compose.yml b/graphql/e2e/auth/docker-compose.yml index 0f5bb630435..04925abaaea 100644 --- a/graphql/e2e/auth/docker-compose.yml +++ b/graphql/e2e/auth/docker-compose.yml @@ -32,7 +32,7 @@ services: labels: cluster: test service: alpha1 - command: /gobin/dgraph alpha --lru_mb=1024 --zero=zero1:5180 -o 100 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=3 --whitelist 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 --my=alpha1:7180 + command: /gobin/dgraph alpha --lru_mb=1024 --zero=zero1:5180 -o 100 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=3 --whitelist 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 --my=alpha1:7180 --graphql_debug=true zeroAdmin: image: dgraph/dgraph:latest @@ -66,4 +66,4 @@ services: labels: cluster: admintest service: alphaAdmin - command: /gobin/dgraph alpha --lru_mb=1024 --zero=zeroAdmin:5280 -o 200 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --whitelist 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 --my=alphaAdmin:7280 + command: /gobin/dgraph alpha --lru_mb=1024 --zero=zeroAdmin:5280 -o 200 --expose_trace --trace 1.0 --profile_mode block --block_rate 10 --logtostderr -v=2 --whitelist 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 --my=alphaAdmin:7280 --graphql_debug=true diff --git a/graphql/e2e/auth/schema.graphql b/graphql/e2e/auth/schema.graphql index 28951209a20..1bbfac7b262 100644 --- a/graphql/e2e/auth/schema.graphql +++ b/graphql/e2e/auth/schema.graphql @@ -23,6 +23,21 @@ type User @auth( tickets: [Ticket] @hasInverse(field: assignedTo) secrets: [UserSecret] issues: [Issue] + tweets: [Tweets] @hasInverse(field: user) +} + +type Tweets @auth ( + query: { rule: "{$ROLE: { eq: \"admin\" } }"}, + add: { rule: "{$USER: { eq: \"foo\" } }"}, + delete: { rule: "{$USER: { eq: \"foo\" } }"}, + update: { rule: "{$USER: { eq: \"foo\" } }"} +){ + id: String! @id + text: String! @search(by: [fulltext]) + user: User + timestamp: DateTime! @search + score: Int @search + streams: String @search } type UserSecret @auth( @@ -537,34 +552,35 @@ type AdminTask @auth( ) { id: ID! name: String @search(by: [exact, term, fulltext, regexp]) - occurrances: [TaskOccurance] @hasInverse(field: adminTask) + occurrences: [TaskOccurrence] @hasInverse(field: adminTask) forContact: Contact @hasInverse(field: adminTasks) } type Task { id: ID! name: String @search(by: [exact, term, fulltext, regexp]) - occurrances: [TaskOccurance] @hasInverse(field: task) + occurrences: [TaskOccurrence] @hasInverse(field: task) forContact: Contact @hasInverse(field: tasks) } -type TaskOccurance @auth( - query: { and : [ - {rule: "{$TaskOccuranceRole: { eq: \"ADMINISTRATOR\"}}"}, - {rule: """ - query($TaskOccuranceRole: String!) { - queryTaskOccurance(filter: {role: { eq: $TaskOccuranceRole}}) { - __typename +type TaskOccurrence @auth( + query: { or : [ { rule: "{$ROLE: { eq: \"ADMIN\" }}"}, + {and : [ + {rule: "{$TaskOccuranceRole: { eq: \"ADMINISTRATOR\"}}"}, + {rule: """ + query($TaskOccuranceRole: String!) { + queryTaskOccurrence(filter: {role: { eq: $TaskOccuranceRole}}) { + __typename + } } - } - """} -] } + """} +] } ] } ) { id: ID! due: DateTime @search comp: DateTime @search - task: Task @hasInverse(field: occurrances) - adminTask: AdminTask @hasInverse(field: occurrances) + task: Task @hasInverse(field: occurrences) + adminTask: AdminTask @hasInverse(field: occurrences) isPublic: Boolean @search role: String @search(by: [exact, term, fulltext, regexp]) } diff --git a/graphql/e2e/common/admin.go b/graphql/e2e/common/admin.go index c6a0f0c3b99..063488d3a9d 100644 --- a/graphql/e2e/common/admin.go +++ b/graphql/e2e/common/admin.go @@ -743,7 +743,7 @@ func testCors(t *testing.T) { time.Sleep(2 * time.Second) client := &http.Client{} - req, err := http.NewRequest("GET", graphqlURL, nil) + req, err := http.NewRequest("GET", GraphqlURL, nil) require.NoError(t, err) req.Header.Add("Origin", "google.com") resp, err := client.Do(req) @@ -754,7 +754,7 @@ func testCors(t *testing.T) { require.Equal(t, resp.Header.Get("Access-Control-Allow-Credentials"), "true") client = &http.Client{} - req, err = http.NewRequest("GET", graphqlURL, nil) + req, err = http.NewRequest("GET", GraphqlURL, nil) require.NoError(t, err) req.Header.Add("Origin", "googl.com") resp, err = client.Do(req) diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index 5a8d2e21ad3..1e042e8055a 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -30,6 +30,7 @@ import ( "github.com/dgraph-io/dgo/v200" "github.com/dgraph-io/dgo/v200/protos/api" + "github.com/dgraph-io/dgraph/testutil" "github.com/dgraph-io/dgraph/x" "github.com/pkg/errors" "github.com/stretchr/testify/require" @@ -37,7 +38,7 @@ import ( ) const ( - graphqlURL = "http://localhost:8180/graphql" + GraphqlURL = "http://localhost:8180/graphql" graphqlAdminURL = "http://localhost:8180/admin" AlphagRPC = "localhost:9180" @@ -99,6 +100,20 @@ type GraphQLResponse struct { Extensions map[string]interface{} `json:"extensions,omitempty"` } +type Tweets struct { + Id string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + User *User `json:"user,omitempty"` +} + +type User struct { + Username string `json:"username,omitempty"` + Age uint64 `json:"age,omitempty"` + IsPublic bool `json:"isPublic,omitempty"` + Disabled bool `json:"disabled,omitempty"` +} + type country struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` @@ -171,6 +186,46 @@ type student struct { TaughtBy []*teacher `json:"taughtBy,omitempty"` } +type UserSecret struct { + Id string `json:"id,omitempty"` + ASecret string `json:"aSecret,omitempty"` + OwnedBy string `json:"ownedBy,omitempty"` +} + +func (twt *Tweets) DeleteByID(t *testing.T, user string, metaInfo *testutil.AuthMeta) { + getParams := &GraphQLParams{ + Headers: GetJWT(t, user, "", metaInfo), + Query: ` + mutation delTweets ($filter : TweetsFilter!){ + deleteTweets (filter: $filter) { + numUids + } + } + `, + Variables: map[string]interface{}{"filter": map[string]interface{}{ + "id": map[string]interface{}{"eq": twt.Id}, + }}, + } + gqlResponse := getParams.ExecuteAsPost(t, GraphqlURL) + require.Nil(t, gqlResponse.Errors) +} + +func (us *UserSecret) Delete(t *testing.T, user, role string, metaInfo *testutil.AuthMeta) { + getParams := &GraphQLParams{ + Headers: GetJWT(t, user, role, metaInfo), + Query: ` + mutation deleteUserSecret($ids: [ID!]) { + deleteUserSecret(filter:{id:$ids}) { + msg + } + } + `, + Variables: map[string]interface{}{"ids": []string{us.Id}}, + } + gqlResponse := getParams.ExecuteAsPost(t, GraphqlURL) + require.Nil(t, gqlResponse.Errors) +} + func BootstrapServer(schema, data []byte) { err := checkGraphQLStarted(graphqlAdminURL) if err != nil { @@ -302,6 +357,7 @@ func RunAll(t *testing.T) { t.Run("add multiple mutations", testMultipleMutations) t.Run("deep XID mutations", deepXIDMutations) t.Run("three level xid", testThreeLevelXID) + t.Run("nested add mutation with @hasInverse", nestedAddMutationWithHasInverse) t.Run("error in multiple mutations", addMultipleMutationWithOneError) t.Run("dgraph directive with reverse edge adds data correctly", addMutationWithReverseDgraphEdge) @@ -374,7 +430,7 @@ func gzipCompressionHeader(t *testing.T) { }`, } - req, err := queryCountry.createGQLPost(graphqlURL) + req, err := queryCountry.createGQLPost(GraphqlURL) require.NoError(t, err) req.Header.Set("Content-Encoding", "gzip") @@ -401,7 +457,7 @@ func gzipCompressionNoHeader(t *testing.T) { gzipEncoding: true, } - req, err := queryCountry.createGQLPost(graphqlURL) + req, err := queryCountry.createGQLPost(GraphqlURL) require.NoError(t, err) req.Header.Del("Content-Encoding") @@ -427,7 +483,7 @@ func getQueryEmptyVariable(t *testing.T) { } }`, } - req, err := queryCountry.createGQLGet(graphqlURL) + req, err := queryCountry.createGQLGet(GraphqlURL) require.NoError(t, err) q := req.URL.Query() @@ -637,7 +693,7 @@ func allCountriesAdded() ([]*country, error) { return nil, errors.Wrap(err, "unable to build GraphQL query") } - req, err := http.NewRequest("POST", graphqlURL, bytes.NewBuffer(body)) + req, err := http.NewRequest("POST", GraphqlURL, bytes.NewBuffer(body)) if err != nil { return nil, errors.Wrap(err, "unable to build GraphQL request") } @@ -801,3 +857,22 @@ func addSchemaThroughAdminSchemaEndpt(url, schema string) error { return nil } + +func GetJWT(t *testing.T, user, role string, metaInfo *testutil.AuthMeta) http.Header { + metaInfo.AuthVars = map[string]interface{}{} + if user != "" { + metaInfo.AuthVars["USER"] = user + } + + if role != "" { + metaInfo.AuthVars["ROLE"] = role + } + + require.NotNil(t, metaInfo.PrivateKeyPath) + jwtToken, err := metaInfo.GetSignedToken(metaInfo.PrivateKeyPath, 300*time.Second) + require.NoError(t, err) + + h := make(http.Header) + h.Add(metaInfo.Header, jwtToken) + return h +} diff --git a/graphql/e2e/common/error.go b/graphql/e2e/common/error.go index b16ac7aec8f..d2818ffc5aa 100644 --- a/graphql/e2e/common/error.go +++ b/graphql/e2e/common/error.go @@ -78,7 +78,7 @@ func graphQLCompletionOn(t *testing.T) { } // Check that the error is valid - gqlResponse := queryCountry.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCountry.ExecuteAsPost(t, GraphqlURL) require.NotNil(t, gqlResponse.Errors) require.Equal(t, 1, len(gqlResponse.Errors)) require.Contains(t, gqlResponse.Errors[0].Error(), @@ -166,7 +166,7 @@ func deepMutationErrors(t *testing.T) { }, } - gqlResponse := executeRequest(t, graphqlURL, updateCountryParams) + gqlResponse := executeRequest(t, GraphqlURL, updateCountryParams) require.NotNil(t, gqlResponse.Errors) require.Equal(t, 1, len(gqlResponse.Errors)) require.EqualError(t, gqlResponse.Errors[0], tcase.exp) @@ -192,7 +192,7 @@ func requestValidationErrors(t *testing.T) { Query: tcase.GQLRequest, Variables: tcase.variables, } - gqlResponse := test.ExecuteAsPost(t, graphqlURL) + gqlResponse := test.ExecuteAsPost(t, GraphqlURL) require.Nil(t, gqlResponse.Data) if diff := cmp.Diff(tcase.Errors, gqlResponse.Errors); diff != "" { diff --git a/graphql/e2e/common/fragment.go b/graphql/e2e/common/fragment.go index de91dc515ae..36ded2fd2a2 100644 --- a/graphql/e2e/common/fragment.go +++ b/graphql/e2e/common/fragment.go @@ -47,7 +47,7 @@ func fragmentInMutation(t *testing.T) { }}, } - gqlResponse := addStarshipParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := addStarshipParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) addStarshipExpected := `{"addStarship":{ @@ -99,7 +99,7 @@ func fragmentInQuery(t *testing.T) { }, } - gqlResponse := queryStarshipParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryStarshipParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) queryStarshipExpected := fmt.Sprintf(` @@ -220,7 +220,7 @@ func fragmentInQueryOnInterface(t *testing.T) { `, } - gqlResponse := queryCharacterParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCharacterParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) queryCharacterExpected := fmt.Sprintf(` @@ -341,7 +341,7 @@ func fragmentInQueryOnObject(t *testing.T) { `, } - gqlResponse := queryHumanParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryHumanParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) queryCharacterExpected := fmt.Sprintf(` diff --git a/graphql/e2e/common/mutation.go b/graphql/e2e/common/mutation.go index 24ed5f04bc5..771fcb1d2e9 100644 --- a/graphql/e2e/common/mutation.go +++ b/graphql/e2e/common/mutation.go @@ -94,7 +94,7 @@ func addCountry(t *testing.T, executeRequest requestExecutor) *country { addCountryExpected := ` { "addCountry": { "country": [{ "id": "_UID_", "name": "Testland" }] } }` - gqlResponse := executeRequest(t, graphqlURL, addCountryParams) + gqlResponse := executeRequest(t, GraphqlURL, addCountryParams) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -139,7 +139,7 @@ func requireCountry(t *testing.T, uid string, expectedCountry *country, includeS }`, Variables: map[string]interface{}{"id": uid, "includeStates": includeStates}, } - gqlResponse := executeRequest(t, graphqlURL, params) + gqlResponse := executeRequest(t, GraphqlURL, params) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -197,7 +197,7 @@ func addAuthor(t *testing.T, countryUID string, }] } }`, countryUID) - gqlResponse := executeRequest(t, graphqlURL, addAuthorParams) + gqlResponse := executeRequest(t, GraphqlURL, addAuthorParams) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -249,7 +249,7 @@ func requireAuthor(t *testing.T, authorID string, expectedAuthor *author, }`, Variables: map[string]interface{}{"id": authorID}, } - gqlResponse := executeRequest(t, graphqlURL, params) + gqlResponse := executeRequest(t, GraphqlURL, params) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -278,7 +278,7 @@ func addCategory(t *testing.T, executeRequest requestExecutor) *category { addCategoryExpected := ` { "addCategory": { "category": [{ "id": "_UID_", "name": "A Category" }] } }` - gqlResponse := executeRequest(t, graphqlURL, addCategoryParams) + gqlResponse := executeRequest(t, GraphqlURL, addCategoryParams) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -400,7 +400,7 @@ func deepMutationsTest(t *testing.T, executeRequest requestExecutor) { }, } - gqlResponse := executeRequest(t, graphqlURL, updateAuthorParams) + gqlResponse := executeRequest(t, GraphqlURL, updateAuthorParams) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -536,7 +536,7 @@ func addMultipleAuthorFromRef(t *testing.T, newAuthor []*author, Variables: map[string]interface{}{"author": newAuthor}, } - gqlResponse := executeRequest(t, graphqlURL, addAuthorParams) + gqlResponse := executeRequest(t, GraphqlURL, addAuthorParams) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -586,7 +586,7 @@ func addComments(t *testing.T, ids []string) { }, } - gqlResponse := postExecutor(t, graphqlURL, params) + gqlResponse := postExecutor(t, GraphqlURL, params) RequireNoGQLErrors(t, gqlResponse) } @@ -810,7 +810,7 @@ func testThreeLevelXID(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { addComments(t, tc.Comments) - gqlResponse := postExecutor(t, graphqlURL, addPostParams) + gqlResponse := postExecutor(t, GraphqlURL, addPostParams) RequireNoGQLErrors(t, gqlResponse) testutil.CompareJSON(t, tc.Expected, string(gqlResponse.Data)) @@ -854,7 +854,7 @@ func deepXIDTest(t *testing.T, executeRequest requestExecutor) { Variables: map[string]interface{}{"input": newCountry}, } - gqlResponse := executeRequest(t, graphqlURL, addCountryParams) + gqlResponse := executeRequest(t, GraphqlURL, addCountryParams) RequireNoGQLErrors(t, gqlResponse) var addResult struct { @@ -924,7 +924,7 @@ func deepXIDTest(t *testing.T, executeRequest requestExecutor) { }, } - gqlResponse = executeRequest(t, graphqlURL, updateCountryParams) + gqlResponse = executeRequest(t, GraphqlURL, updateCountryParams) RequireNoGQLErrors(t, gqlResponse) var updResult struct { @@ -1008,7 +1008,7 @@ func addPost(t *testing.T, authorID, countryID string, }] } }`, authorID, countryID) - gqlResponse := executeRequest(t, graphqlURL, addPostParams) + gqlResponse := executeRequest(t, GraphqlURL, addPostParams) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -1063,7 +1063,7 @@ func requirePost( }, } - gqlResponse := executeRequest(t, graphqlURL, params) + gqlResponse := executeRequest(t, GraphqlURL, params) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -1177,7 +1177,7 @@ func updateRemove(t *testing.T) { Variables: map[string]interface{}{"filter": filter, "rem": remPatch}, } - gqlResponse := updateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := updateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, `{ @@ -1216,7 +1216,7 @@ func updateCountry(t *testing.T, filter map[string]interface{}, newName string, Variables: map[string]interface{}{"filter": filter, "newName": newName}, } - gqlResponse := updateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := updateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -1298,7 +1298,7 @@ func filterInUpdate(t *testing.T) { }, } - gqlResponse := updateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := updateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -1403,7 +1403,7 @@ func addMutationUpdatesRefs(t *testing.T, executeRequest requestExecutor) { "posts": []interface{}{newPost}, }}, } - gqlResponse := executeRequest(t, graphqlURL, addAuthorParams) + gqlResponse := executeRequest(t, GraphqlURL, addAuthorParams) RequireNoGQLErrors(t, gqlResponse) var addResult struct { @@ -1450,7 +1450,7 @@ func addMutationUpdatesRefsXID(t *testing.T, executeRequest requestExecutor) { Variables: map[string]interface{}{"input": newCountry}, } - gqlResponse := executeRequest(t, graphqlURL, addCountryParams) + gqlResponse := executeRequest(t, GraphqlURL, addCountryParams) RequireNoGQLErrors(t, gqlResponse) var addResult struct { @@ -1509,7 +1509,7 @@ func updateMutationUpdatesRefs(t *testing.T, executeRequest requestExecutor) { "set": map[string]interface{}{"posts": []interface{}{newPost}}, }, } - gqlResponse := executeRequest(t, graphqlURL, updateAuthorParams) + gqlResponse := executeRequest(t, GraphqlURL, updateAuthorParams) RequireNoGQLErrors(t, gqlResponse) // The original author no longer has newPost in its list of posts @@ -1560,7 +1560,7 @@ func updateMutationOnlyUpdatesRefsIfDifferent(t *testing.T, executeRequest reque "author": newAuthor}, }, } - gqlResponse := executeRequest(t, graphqlURL, updateAuthorParams) + gqlResponse := executeRequest(t, GraphqlURL, updateAuthorParams) RequireNoGQLErrors(t, gqlResponse) // The expected post was updated @@ -1598,7 +1598,7 @@ func updateMutationUpdatesRefsXID(t *testing.T, executeRequest requestExecutor) Variables: map[string]interface{}{"input": newCountry}, } - gqlResponse := executeRequest(t, graphqlURL, addCountryParams) + gqlResponse := executeRequest(t, GraphqlURL, addCountryParams) RequireNoGQLErrors(t, gqlResponse) var addResult struct { @@ -1632,7 +1632,7 @@ func updateMutationUpdatesRefsXID(t *testing.T, executeRequest requestExecutor) }, } - gqlResponse = executeRequest(t, graphqlURL, updateCountryParams) + gqlResponse = executeRequest(t, GraphqlURL, updateCountryParams) RequireNoGQLErrors(t, gqlResponse) // newCountry doesn't have "ABC" in it's states list @@ -1676,7 +1676,7 @@ func deleteMutationSingleReference(t *testing.T, executeRequest requestExecutor) Variables: map[string]interface{}{"input": newCountry}, } - gqlResponse := executeRequest(t, graphqlURL, addCountryParams) + gqlResponse := executeRequest(t, GraphqlURL, addCountryParams) RequireNoGQLErrors(t, gqlResponse) var addResult struct { @@ -1700,7 +1700,7 @@ func deleteMutationSingleReference(t *testing.T, executeRequest requestExecutor) }`, Variables: map[string]interface{}{"id": addResult.AddCountry.Country[0].States[0].ID}, } - gqlResponse = getCatParams.ExecuteAsPost(t, graphqlURL) + gqlResponse = getCatParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, `{"getState":{"country":null}}`, string(gqlResponse.Data)) @@ -1723,7 +1723,7 @@ func deleteMutationMultipleReferences(t *testing.T, executeRequest requestExecut "set": map[string]interface{}{"category": newCategory}}, } - gqlResponse := updateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := updateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) // show that this post is in the author's posts @@ -1751,7 +1751,7 @@ func deleteMutationMultipleReferences(t *testing.T, executeRequest requestExecut }`, Variables: map[string]interface{}{"id": newCategory.ID}, } - gqlResponse = getCatParams.ExecuteAsPost(t, graphqlURL) + gqlResponse = getCatParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, `{"getCategory":{"posts":[]}}`, string(gqlResponse.Data)) @@ -1805,7 +1805,7 @@ func deleteWrongID(t *testing.T) { Variables: map[string]interface{}{"filter": filter}, } - gqlResponse := deleteCountryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := deleteCountryParams.ExecuteAsPost(t, GraphqlURL) require.JSONEq(t, expectedData, string(gqlResponse.Data)) cleanUp(t, []*country{newCountry}, []*author{newAuthor}, []*post{}) @@ -1841,7 +1841,7 @@ func manyMutations(t *testing.T) { "add2": { "country": [{ "id": "_UID_", "name": "Testland2" }] } }` - gqlResponse := multiMutationParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := multiMutationParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -1938,7 +1938,7 @@ func testSelectionInAddObject(t *testing.T) { }, } - gqlResponse := postExecutor(t, graphqlURL, addPostParams) + gqlResponse := postExecutor(t, GraphqlURL, addPostParams) RequireNoGQLErrors(t, gqlResponse) var result struct { AddPost struct { @@ -1978,7 +1978,7 @@ func mutationEmptyDelete(t *testing.T) { }`, } - gqlResponse := updatePostParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := updatePostParams.ExecuteAsPost(t, GraphqlURL) require.NotNil(t, gqlResponse.Errors) require.Equal(t, gqlResponse.Errors[0].Error(), "couldn't rewrite mutation updatePost"+ " because failed to rewrite mutation payload because id is not provided") @@ -2027,7 +2027,7 @@ func mutationWithDeepFilter(t *testing.T) { }] } }` - gqlResponse := addPostParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := addPostParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -2122,7 +2122,7 @@ func manyMutationsWithQueryError(t *testing.T) { Locations: []x.Location{{Line: 18, Column: 7}}, Path: []interface{}{"add2", "author", float64(0), "country", "name"}}} - gqlResponse := multiMutationParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := multiMutationParams.ExecuteAsPost(t, GraphqlURL) if diff := cmp.Diff(expectedErrors, gqlResponse.Errors); diff != "" { t.Errorf("errors mismatch (-want +got):\n%s", diff) @@ -2197,7 +2197,7 @@ func addStarship(t *testing.T) *starship { }}, } - gqlResponse := addStarshipParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := addStarshipParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) addStarshipExpected := fmt.Sprintf(`{"addStarship":{ @@ -2247,7 +2247,7 @@ func addHuman(t *testing.T, starshipID string) string { }}, } - gqlResponse := addHumanParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := addHumanParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -2280,7 +2280,7 @@ func addDroid(t *testing.T) string { }}, } - gqlResponse := addDroidParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := addDroidParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -2313,7 +2313,7 @@ func addThingOne(t *testing.T) string { }}, } - gqlResponse := addDroidParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := addDroidParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -2346,7 +2346,7 @@ func addThingTwo(t *testing.T) string { }}, } - gqlResponse := addDroidParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := addDroidParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -2392,7 +2392,7 @@ func updateCharacter(t *testing.T, id string) { }}, } - gqlResponse := updateCharacterParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := updateCharacterParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) } @@ -2423,7 +2423,7 @@ func queryInterfaceAfterAddMutation(t *testing.T) { }`, } - gqlResponse := queryCharacterParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCharacterParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := fmt.Sprintf(`{ @@ -2473,7 +2473,7 @@ func queryInterfaceAfterAddMutation(t *testing.T) { }`, } - gqlResponse := queryCharacterByNameParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCharacterByNameParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := fmt.Sprintf(`{ @@ -2511,7 +2511,7 @@ func queryInterfaceAfterAddMutation(t *testing.T) { }`, } - gqlResponse := queryHumanParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryHumanParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := fmt.Sprintf(`{ @@ -2549,7 +2549,7 @@ func queryInterfaceAfterAddMutation(t *testing.T) { }`, } - gqlResponse := queryHumanParamsByName.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryHumanParamsByName.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := fmt.Sprintf(`{ @@ -2608,7 +2608,7 @@ func requireState(t *testing.T, uid string, expectedState *state, }`, Variables: map[string]interface{}{"id": uid}, } - gqlResponse := executeRequest(t, graphqlURL, params) + gqlResponse := executeRequest(t, GraphqlURL, params) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -2638,7 +2638,7 @@ func addState(t *testing.T, name string, executeRequest requestExecutor) *state addStateExpected := ` { "addState": { "state": [{ "id": "_UID_", "name": "` + name + `", "xcode": "cal" } ]} }` - gqlResponse := executeRequest(t, graphqlURL, addStateParams) + gqlResponse := executeRequest(t, GraphqlURL, addStateParams) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -2685,7 +2685,7 @@ func deleteGqlType( Variables: map[string]interface{}{"filter": filter}, } - gqlResponse := deleteTypeParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := deleteTypeParams.ExecuteAsPost(t, GraphqlURL) if len(expectedErrors) == 0 { RequireNoGQLErrors(t, gqlResponse) @@ -2732,7 +2732,7 @@ func addMutationWithXid(t *testing.T, executeRequest requestExecutor) { Variables: map[string]interface{}{"name": name, "xcode": "cal"}, } - gqlResponse := executeRequest(t, graphqlURL, addStateParams) + gqlResponse := executeRequest(t, GraphqlURL, addStateParams) require.NotNil(t, gqlResponse.Errors) require.Contains(t, gqlResponse.Errors[0].Error(), "because id cal already exists for type State") @@ -2793,7 +2793,7 @@ func addMultipleMutationWithOneError(t *testing.T) { anotherGoodPost}}, } - gqlResponse := postExecutor(t, graphqlURL, addPostParams) + gqlResponse := postExecutor(t, GraphqlURL, addPostParams) addPostExpected := fmt.Sprintf(`{ "addPost": { "post": [{ @@ -2843,7 +2843,7 @@ func addMovie(t *testing.T, executeRequest requestExecutor) *movie { addMovieExpected := ` { "addMovie": { "movie": [{ "id": "_UID_", "name": "Testmovie", "director": [] }] } }` - gqlResponse := executeRequest(t, graphqlURL, addMovieParams) + gqlResponse := executeRequest(t, GraphqlURL, addMovieParams) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -2890,7 +2890,7 @@ func cleanupMovieAndDirector(t *testing.T, movieID, directorID string) { "deleteMovieDirector" : { "msg": "Deleted" } }` - gqlResponse := multiMutationParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := multiMutationParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) testutil.CompareJSON(t, multiMutationExpected, string(gqlResponse.Data)) @@ -2920,7 +2920,7 @@ func addMutationWithReverseDgraphEdge(t *testing.T) { addMovieDirectorExpected := `{ "addMovieDirector": { "movieDirector": [{ "id": "_UID_", "name": "Spielberg" }] } }` - gqlResponse := postExecutor(t, graphqlURL, addMovieDirectorParams) + gqlResponse := postExecutor(t, GraphqlURL, addMovieDirectorParams) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -2958,7 +2958,7 @@ func addMutationWithReverseDgraphEdge(t *testing.T) { }, } - gqlResponse = getMovieParams.ExecuteAsPost(t, graphqlURL) + gqlResponse = getMovieParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expectedResponse := `{"getMovie":{"name":"Testmovie","director":[{"name":"Spielberg"}]}}` require.Equal(t, expectedResponse, string(gqlResponse.Data)) @@ -3009,7 +3009,7 @@ func testNumUids(t *testing.T) { } } - gqlResponse := postExecutor(t, graphqlURL, addAuthorParams) + gqlResponse := postExecutor(t, GraphqlURL, addAuthorParams) RequireNoGQLErrors(t, gqlResponse) t.Run("Test numUID in add", func(t *testing.T) { @@ -3037,7 +3037,7 @@ func testNumUids(t *testing.T) { }}, } - gqlResponse = postExecutor(t, graphqlURL, updatePostParams) + gqlResponse = postExecutor(t, GraphqlURL, updatePostParams) RequireNoGQLErrors(t, gqlResponse) var updateResult struct { @@ -3077,7 +3077,7 @@ func testNumUids(t *testing.T) { }, }, } - gqlResponse = postExecutor(t, graphqlURL, deleteAuthorParams) + gqlResponse = postExecutor(t, GraphqlURL, deleteAuthorParams) RequireNoGQLErrors(t, gqlResponse) var deleteResult struct { @@ -3114,7 +3114,7 @@ func checkUser(t *testing.T, userObj, expectedObj *user) { }, } - gqlResponse := checkUserParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := checkUserParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -3172,7 +3172,7 @@ func passwordTest(t *testing.T) { } t.Run("Test add and update user", func(t *testing.T) { - gqlResponse := postExecutor(t, graphqlURL, addUserParams) + gqlResponse := postExecutor(t, GraphqlURL, addUserParams) RequireNoGQLErrors(t, gqlResponse) require.Equal(t, `{"addUser":{"user":[{"name":"Test User"}]}}`, string(gqlResponse.Data)) @@ -3180,7 +3180,7 @@ func passwordTest(t *testing.T) { checkUser(t, newUser, newUser) checkUser(t, &user{Name: "Test User", Password: "Wrong Pass"}, nil) - gqlResponse = postExecutor(t, graphqlURL, updateUserParams) + gqlResponse = postExecutor(t, GraphqlURL, updateUserParams) RequireNoGQLErrors(t, gqlResponse) require.Equal(t, `{"updateUser":{"user":[{"name":"Test User"}]}}`, string(gqlResponse.Data)) @@ -3237,7 +3237,7 @@ func threeLevelDeepMutation(t *testing.T) { Variables: map[string]interface{}{"input": newStudents}, } - gqlResponse := postExecutor(t, graphqlURL, addStudentParams) + gqlResponse := postExecutor(t, GraphqlURL, addStudentParams) RequireNoGQLErrors(t, gqlResponse) var actualResult struct { @@ -3313,7 +3313,7 @@ func deepMutationDuplicateXIDsSameObjectTest(t *testing.T) { Variables: map[string]interface{}{"input": newStudents}, } - gqlResponse := postExecutor(t, graphqlURL, addStudentParams) + gqlResponse := postExecutor(t, GraphqlURL, addStudentParams) RequireNoGQLErrors(t, gqlResponse) var actualResult struct { @@ -3400,7 +3400,7 @@ func queryTypenameInMutationPayload(t *testing.T) { }`, } - gqlResponse := addStateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := addStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) addStateExpected := `{ @@ -3434,7 +3434,7 @@ func ensureAliasInMutationPayload(t *testing.T) { }`, } - gqlResponse := addStateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := addStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) addStateExpected := `{ @@ -3463,7 +3463,7 @@ func mutationsHaveExtensions(t *testing.T) { } touchedUidskey := "touched_uids" - gqlResponse := mutation.ExecuteAsPost(t, graphqlURL) + gqlResponse := mutation.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.Contains(t, gqlResponse.Extensions, touchedUidskey) require.Greater(t, int(gqlResponse.Extensions[touchedUidskey].(float64)), 0) @@ -3508,7 +3508,7 @@ func mutationsWithAlias(t *testing.T) { "del" : { "message": "Deleted", "uids": 1 } }` - gqlResponse := aliasMutationParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := aliasMutationParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, multiMutationExpected, string(gqlResponse.Data)) @@ -3529,7 +3529,7 @@ func updateMutationWithoutSetRemove(t *testing.T) { }`, Variables: map[string]interface{}{"id": country.ID}, } - gqlResponse := updateCountryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := updateCountryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, `{ @@ -3542,3 +3542,68 @@ func updateMutationWithoutSetRemove(t *testing.T) { // cleanup deleteCountry(t, map[string]interface{}{"id": []string{country.ID}}, 1, nil) } + +func nestedAddMutationWithHasInverse(t *testing.T) { + params := &GraphQLParams{ + Query: `mutation addPerson1($input: [AddPerson1Input!]!) { + addPerson1(input: $input) { + person1 { + name + friends { + name + friends { + name + } + } + } + } + }`, + Variables: map[string]interface{}{ + "input": []interface{}{ + map[string]interface{}{ + "name": "Or", + "friends": []interface{}{ + map[string]interface{}{ + "name": "Michal", + "friends": []interface{}{ + map[string]interface{}{ + "name": "Justin", + }, + }, + }, + }, + }, + }, + }, + } + + gqlResponse := postExecutor(t, GraphqlURL, params) + RequireNoGQLErrors(t, gqlResponse) + + expected := `{ + "addPerson1": { + "person1": [ + { + "friends": [ + { + "friends": [ + { + "name": "Or" + }, + { + "name": "Justin" + } + ], + "name": "Michal" + } + ], + "name": "Or" + } + ] + } + }` + testutil.CompareJSON(t, expected, string(gqlResponse.Data)) + + // cleanup + deleteGqlType(t, "Person1", map[string]interface{}{}, 3, nil) +} diff --git a/graphql/e2e/common/query.go b/graphql/e2e/common/query.go index f8ed82ff82a..f0da872188c 100644 --- a/graphql/e2e/common/query.go +++ b/graphql/e2e/common/query.go @@ -44,7 +44,7 @@ func queryCountryByRegExp(t *testing.T, regexp string, expectedCountries []*coun Variables: map[string]interface{}{"regexp": regexp}, } - gqlResponse := getCountryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getCountryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -72,7 +72,7 @@ func touchedUidsHeader(t *testing.T) { } }`, } - req, err := query.createGQLPost(graphqlURL) + req, err := query.createGQLPost(GraphqlURL) require.NoError(t, err) client := http.Client{Timeout: 10 * time.Second} @@ -122,7 +122,7 @@ func queryByTypeWithEncoding(t *testing.T, acceptGzip, gzipEncoding bool) { gzipEncoding: gzipEncoding, } - gqlResponse := queryCountry.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCountry.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -157,7 +157,7 @@ func uidAlias(t *testing.T) { UID string } - gqlResponse := queryCountry.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCountry.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -185,7 +185,7 @@ func orderAtRoot(t *testing.T) { }`, } - gqlResponse := queryCountry.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCountry.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -213,7 +213,7 @@ func pageAtRoot(t *testing.T) { }`, } - gqlResponse := queryCountry.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCountry.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -257,7 +257,7 @@ func multipleSearchIndexes(t *testing.T) { Variables: map[string]interface{}{"filter": filter}, } - gqlResponse := getCountryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getCountryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -285,7 +285,7 @@ func multipleSearchIndexesWrongField(t *testing.T) { }`, } - gqlResponse := queryPostParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryPostParams.ExecuteAsPost(t, GraphqlURL) require.NotNil(t, gqlResponse.Errors) expected := `Field "regexp" is not defined by type StringFullTextFilter_StringTermFilter` @@ -302,7 +302,7 @@ func hashSearch(t *testing.T) { }`, } - gqlResponse := queryAuthorParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryAuthorParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var expected, result struct { @@ -332,7 +332,7 @@ func allPosts(t *testing.T) []*post { } }`, } - gqlResponse := queryPostParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryPostParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -358,7 +358,7 @@ func deepFilter(t *testing.T) { }`, } - gqlResponse := getAuthorParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getAuthorParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -407,7 +407,7 @@ func manyQueries(t *testing.T) { Query: bld.String(), } - gqlResponse := queryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result map[string]*post @@ -510,7 +510,7 @@ func queryOrderAtRoot(t *testing.T) { }, } - gqlResponse := getParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected.QueryPost = test.Expected @@ -562,7 +562,7 @@ func queriesWithError(t *testing.T) { Query: bld.String(), } - gqlResponse := queryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryParams.ExecuteAsPost(t, GraphqlURL) require.Len(t, gqlResponse.Errors, 1, "expected 1 error from malformed query") var result map[string]*post @@ -649,7 +649,7 @@ func authorTest(t *testing.T, filter interface{}, expected []*author) { Variables: map[string]interface{}{"filter": filter}, } - gqlResponse := queryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -862,7 +862,7 @@ func postTest(t *testing.T, filter interface{}, expected []*post) { Variables: map[string]interface{}{"filter": filter}, } - gqlResponse := queryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -894,7 +894,7 @@ func skipDirective(t *testing.T) { }, } - gqlResponse := getAuthorParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getAuthorParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := `{"queryAuthor":[{"name":"Ann Other Author", @@ -919,7 +919,7 @@ func includeDirective(t *testing.T) { }, } - gqlResponse := getAuthorParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getAuthorParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := `{"queryAuthor":[{"name":"Ann Other Author","dob":"1988-01-01T00:00:00Z"}]}` @@ -949,7 +949,7 @@ func includeAndSkipDirective(t *testing.T) { }, } - gqlResponse := getAuthorParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getAuthorParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := `{"queryAuthor":[{"name":"Ann Other Author"}]}` @@ -980,7 +980,7 @@ func queryByMultipleIds(t *testing.T) { }}, } - gqlResponse := queryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -1048,7 +1048,7 @@ func enumFilter(t *testing.T) { t.Run(name, func(t *testing.T) { queryParams.Variables = map[string]interface{}{"filter": test.Filter} - gqlResponse := queryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -1084,7 +1084,7 @@ func queryApplicationGraphQl(t *testing.T) { }`, } - gqlResponse := getCountryParams.ExecuteAsPostApplicationGraphql(t, graphqlURL) + gqlResponse := getCountryParams.ExecuteAsPostApplicationGraphql(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := `{ @@ -1108,7 +1108,7 @@ func queryTypename(t *testing.T) { }`, } - gqlResponse := getCountryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getCountryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := `{ @@ -1145,7 +1145,7 @@ func queryNestedTypename(t *testing.T) { }`, } - gqlResponse := getCountryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getCountryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := `{ @@ -1210,7 +1210,7 @@ func typenameForInterface(t *testing.T) { ] }` - gqlResponse := queryCharacterParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCharacterParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) testutil.CompareJSON(t, expected, string(gqlResponse.Data)) }) @@ -1221,19 +1221,19 @@ func typenameForInterface(t *testing.T) { func queryOnlyTypename(t *testing.T) { newCountry1 := addCountry(t, postExecutor) - newCountry2 := addCountry(t, postExecutor) - newCountry3 := addCountry(t, postExecutor) + newCountry2 := addCountry(t, postExecutor) + newCountry3 := addCountry(t, postExecutor) getCountryParams := &GraphQLParams{ - Query: `query { + Query: `query { queryCountry(filter: { name: {eq: "Testland"}}) { __typename } }`, - } - - gqlResponse := getCountryParams.ExecuteAsPost(t, graphqlURL) - RequireNoGQLErrors(t, gqlResponse) + } + + gqlResponse := getCountryParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) expected := `{ "queryCountry": [ @@ -1249,20 +1249,20 @@ func queryOnlyTypename(t *testing.T) { ] }` - - require.JSONEq(t, expected, string(gqlResponse.Data)) - cleanUp(t, []*country{newCountry1,newCountry2,newCountry3}, []*author{}, []*post{}) + + require.JSONEq(t, expected, string(gqlResponse.Data)) + cleanUp(t, []*country{newCountry1, newCountry2, newCountry3}, []*author{}, []*post{}) } func querynestedOnlyTypename(t *testing.T) { newCountry := addCountry(t, postExecutor) - newAuthor := addAuthor(t, newCountry.ID, postExecutor) - newPost1 := addPost(t, newAuthor.ID, newCountry.ID, postExecutor) - newPost2 := addPost(t, newAuthor.ID, newCountry.ID, postExecutor) - newPost3 := addPost(t, newAuthor.ID, newCountry.ID, postExecutor) - - getCountryParams := &GraphQLParams{ + newAuthor := addAuthor(t, newCountry.ID, postExecutor) + newPost1 := addPost(t, newAuthor.ID, newCountry.ID, postExecutor) + newPost2 := addPost(t, newAuthor.ID, newCountry.ID, postExecutor) + newPost3 := addPost(t, newAuthor.ID, newCountry.ID, postExecutor) + + getCountryParams := &GraphQLParams{ Query: `query { queryAuthor(filter: { name: { eq: "Test Author" } }) { posts { @@ -1270,12 +1270,12 @@ func querynestedOnlyTypename(t *testing.T) { } } }`, - } - - gqlResponse := getCountryParams.ExecuteAsPost(t, graphqlURL) - RequireNoGQLErrors(t, gqlResponse) + } + + gqlResponse := getCountryParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) - expected := `{ + expected := `{ "queryAuthor": [ { "posts": [ @@ -1293,11 +1293,10 @@ func querynestedOnlyTypename(t *testing.T) { } ] }` - require.JSONEq(t, expected, string(gqlResponse.Data)) - cleanUp(t, []*country{newCountry}, []*author{newAuthor}, []*post{newPost1,newPost2,newPost3}) + require.JSONEq(t, expected, string(gqlResponse.Data)) + cleanUp(t, []*country{newCountry}, []*author{newAuthor}, []*post{newPost1, newPost2, newPost3}) } - func onlytypenameForInterface(t *testing.T) { newStarship := addStarship(t) humanID := addHuman(t, newStarship.ID) @@ -1335,7 +1334,7 @@ func onlytypenameForInterface(t *testing.T) { ] }` - gqlResponse := queryCharacterParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCharacterParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) testutil.CompareJSON(t, expected, string(gqlResponse.Data)) }) @@ -1343,7 +1342,6 @@ func onlytypenameForInterface(t *testing.T) { cleanupStarwars(t, newStarship.ID, humanID, droidID) } - func defaultEnumFilter(t *testing.T) { newStarship := addStarship(t) humanID := addHuman(t, newStarship.ID) @@ -1364,7 +1362,7 @@ func defaultEnumFilter(t *testing.T) { }`, } - gqlResponse := queryCharacterParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryCharacterParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := `{ @@ -1405,7 +1403,7 @@ func queryByMultipleInvalidIds(t *testing.T) { // Since the ids are invalid and can't be converted to uint64, the query sent to Dgraph should // have func: uid() at root and should return 0 results. - gqlResponse := queryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.Equal(t, `{"queryPost":[]}`, string(gqlResponse.Data)) @@ -1426,7 +1424,7 @@ func getStateByXid(t *testing.T) { }`, } - gqlResponse := getStateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.Equal(t, `{"getState":{"name":"NSW"}}`, string(gqlResponse.Data)) } @@ -1440,7 +1438,7 @@ func getStateWithoutArgs(t *testing.T) { }`, } - gqlResponse := getStateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, `{"getState":null}`, string(gqlResponse.Data)) } @@ -1454,7 +1452,7 @@ func getStateByBothXidAndUid(t *testing.T) { }`, } - gqlResponse := getStateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, `{"getState":null}`, string(gqlResponse.Data)) } @@ -1468,7 +1466,7 @@ func queryStateByXid(t *testing.T) { }`, } - gqlResponse := getStateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.Equal(t, `{"queryState":[{"name":"NSW"}]}`, string(gqlResponse.Data)) } @@ -1482,7 +1480,7 @@ func queryStateByXidRegex(t *testing.T) { }`, } - gqlResponse := getStateParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := getStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) testutil.CompareJSON(t, `{"queryState":[{"name":"Nusa"},{"name": "NSW"}]}`, string(gqlResponse.Data)) @@ -1540,7 +1538,7 @@ func multipleOperations(t *testing.T) { for _, test := range cases { t.Run(test.name, func(t *testing.T) { params.OperationName = test.operationName - gqlResponse := params.ExecuteAsPost(t, graphqlURL) + gqlResponse := params.ExecuteAsPost(t, GraphqlURL) if test.expectedError != "" { require.NotNil(t, gqlResponse.Errors) require.Equal(t, test.expectedError, gqlResponse.Errors[0].Error()) @@ -1574,7 +1572,7 @@ func queryPostWithAuthor(t *testing.T) { }`, } - gqlResponse := queryPostParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryPostParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) testutil.CompareJSON(t, `{"queryPost":[{"title":"Introducing GraphQL in Dgraph","author":{"name":"Ann Author"}}]}`, @@ -1591,7 +1589,7 @@ func queriesHaveExtensions(t *testing.T) { } touchedUidskey := "touched_uids" - gqlResponse := query.ExecuteAsPost(t, graphqlURL) + gqlResponse := query.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.Contains(t, gqlResponse.Extensions, touchedUidskey) require.Greater(t, int(gqlResponse.Extensions[touchedUidskey].(float64)), 0) @@ -1611,7 +1609,7 @@ func queryWithAlias(t *testing.T) { }`, } - gqlResponse := queryPostParams.ExecuteAsPost(t, graphqlURL) + gqlResponse := queryPostParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) testutil.CompareJSON(t, `{ @@ -1636,7 +1634,7 @@ func DgraphDirectiveWithSpecialCharacters(t *testing.T) { }`, } result := `{"addMessage":{"message":[{"content":"content1","author":"author1"}]}}` - gqlResponse := mutation.ExecuteAsPost(t, graphqlURL) + gqlResponse := mutation.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, result, string(gqlResponse.Data)) @@ -1650,7 +1648,7 @@ func DgraphDirectiveWithSpecialCharacters(t *testing.T) { }`, } result = `{"queryMessage":[{"content":"content1","author":"author1"}]}` - gqlResponse = queryParams.ExecuteAsPost(t, graphqlURL) + gqlResponse = queryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, result, string(gqlResponse.Data)) } @@ -1696,7 +1694,7 @@ func queryWithCascade(t *testing.T) { }`, Variables: map[string]interface{}{"input": states}, } - resp := addStateParams.ExecuteAsPost(t, graphqlURL) + resp := addStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, resp) testutil.CompareJSON(t, `{"addState":{"numUids":2}}`, string(resp.Data)) getStateByXidQuery := `query ($xid: String!) { @@ -1802,7 +1800,7 @@ func queryWithCascade(t *testing.T) { Query: tcase.query, Variables: tcase.variables, } - resp := params.ExecuteAsPost(t, graphqlURL) + resp := params.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, resp) testutil.CompareJSON(t, tcase.respData, string(resp.Data)) }) diff --git a/graphql/e2e/common/schema.go b/graphql/e2e/common/schema.go index 8848f356d49..ebccc782792 100644 --- a/graphql/e2e/common/schema.go +++ b/graphql/e2e/common/schema.go @@ -152,7 +152,7 @@ func graphQLDescriptions(t *testing.T) { }, } - introspectionResult := introspect.ExecuteAsPost(t, graphqlURL) + introspectionResult := introspect.ExecuteAsPost(t, GraphqlURL) require.Nil(t, introspectionResult.Errors) require.JSONEq(t, tCase.expected, string(introspectionResult.Data)) diff --git a/graphql/e2e/custom_logic/cmd/graphqlresponse.yaml b/graphql/e2e/custom_logic/cmd/graphqlresponse.yaml index 10d45e768e9..91ac57accd7 100644 --- a/graphql/e2e/custom_logic/cmd/graphqlresponse.yaml +++ b/graphql/e2e/custom_logic/cmd/graphqlresponse.yaml @@ -437,24 +437,6 @@ getPosts(input: [PostFilterInput]): [Post!] } -- name: getPostswithLike - schema: | - input PostFilterInput{ - id: ID! - text: String! - likes: Int - } - - type Post { - id: ID! - text: String - comments: Post! - } - - type Query{ - getPosts(input: [PostFilterInput]): [Post!] - } - - name: "carsschema" schema: | type Car { diff --git a/graphql/e2e/custom_logic/cmd/main.go b/graphql/e2e/custom_logic/cmd/main.go index fb4e60d2ca8..5d2c8254661 100644 --- a/graphql/e2e/custom_logic/cmd/main.go +++ b/graphql/e2e/custom_logic/cmd/main.go @@ -639,19 +639,6 @@ func getPosts(w http.ResponseWriter, r *http.Request) { check2(fmt.Fprint(w, generateIntrospectionResult(graphqlResponses["getPosts"].Schema))) } -func getPostswithLike(w http.ResponseWriter, r *http.Request) { - _, err := verifyGraphqlRequest(r, expectedGraphqlRequest{ - urlSuffix: "/getPostswithLike", - body: ``, - }) - if err != nil { - check2(w.Write([]byte(err.Error()))) - return - } - - check2(fmt.Fprint(w, generateIntrospectionResult(graphqlResponses["getPostswithLike"].Schema))) -} - type input struct { ID string `json:"uid"` } @@ -1308,7 +1295,6 @@ func main() { bsch := graphql.MustParseSchema(graphqlResponses["batchOperationSchema"].Schema, &query{}) bh := &relay.Handler{Schema: bsch} http.HandleFunc("/getPosts", getPosts) - http.HandleFunc("/getPostswithLike", getPostswithLike) http.Handle("/gqlUserNames", bh) http.Handle("/gqlCars", bh) http.HandleFunc("/gqlCarsWithErrors", gqlCarsWithErrorHandler) diff --git a/graphql/e2e/custom_logic/custom_logic_test.go b/graphql/e2e/custom_logic/custom_logic_test.go index c733a8a09c2..b70c44f9a2d 100644 --- a/graphql/e2e/custom_logic/custom_logic_test.go +++ b/graphql/e2e/custom_logic/custom_logic_test.go @@ -2215,51 +2215,6 @@ func TestCustomGraphqlMissingTypeForBatchedFieldInput(t *testing.T) { "PostFilterInput.\n") } -func TestCustomGraphqlInvalidArgForBatchedField(t *testing.T) { - t.Skip() - schema := ` - type Post { - id: ID! - text: String - comments: Post! @custom(http: { - url: "http://mock:8888/getPosts", - method: "POST", - mode: BATCH - graphql: "query { getPosts(input: [{name: $id}]) }" - }) - } - ` - res := updateSchema(t, schema) - require.Equal(t, `{"updateGQLSchema":null}`, string(res.Data)) - require.Len(t, res.Errors, 1) - require.Equal(t, "resolving updateGQLSchema failed because input:9: Type Post"+ - "; Field comments: inside graphql in @custom directive, argument `name` is not present "+ - "in remote query `getPosts`.\n", res.Errors[0].Error()) -} - -func TestCustomGraphqlArgTypeMismatchForBatchedField(t *testing.T) { - t.Skip() - schema := ` - type Post { - id: ID! - likes: Int - text: String - comments: Post! @custom(http: { - url: "http://mock:8888/getPostswithLike", - method: "POST", - mode: BATCH - graphql: "query { getPosts(input: [{id: $id, text: $likes}]) }" - }) - } - ` - res := updateSchema(t, schema) - require.Equal(t, `{"updateGQLSchema":null}`, string(res.Data)) - require.Len(t, res.Errors, 1) - require.Equal(t, "resolving updateGQLSchema failed because input:10: Type Post"+ - "; Field comments: inside graphql in @custom directive, found type mismatch for variable"+ - " `$likes` in query `getPosts`, expected `Int`, got `String!`.\n", res.Errors[0].Error()) -} - func TestCustomGraphqlMissingRequiredArgument(t *testing.T) { schema := ` type Country @remote { @@ -2300,28 +2255,6 @@ func TestCustomGraphqlMissingRequiredArgument(t *testing.T) { " `setCountry` is missing, it is required by remote mutation.") } -func TestCustomGraphqlMissingRequiredArgumentForBatchedField(t *testing.T) { - t.Skip() - schema := ` - type Post { - id: ID! - text: String - comments: Post! @custom(http: { - url: "http://mock:8888/getPosts", - method: "POST", - mode: BATCH - graphql: "query { getPosts(input: [{id: $id}]) }" - }) - } - ` - res := updateSchema(t, schema) - require.Equal(t, `{"updateGQLSchema":null}`, string(res.Data)) - require.Len(t, res.Errors, 1) - require.Equal(t, "resolving updateGQLSchema failed because input:9: Type Post"+ - "; Field comments: inside graphql in @custom directive, argument `text` in query "+ - "`getPosts` is missing, it is required by remote query.\n", res.Errors[0].Error()) -} - // this one accepts an object and returns an object func TestCustomGraphqlMutation1(t *testing.T) { schema := ` @@ -2672,7 +2605,7 @@ func TestRestCustomLogicInDeepNestedField(t *testing.T) { result = params.ExecuteAsPost(t, alphaURL) common.RequireNoGQLErrors(t, result) - require.JSONEq(t, string(result.Data), ` + testutil.CompareJSON(t, ` { "querySearchTweets": [ { @@ -2693,7 +2626,7 @@ func TestRestCustomLogicInDeepNestedField(t *testing.T) { } } ] - }`) + }`, string(result.Data)) } func TestCustomDQL(t *testing.T) { diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index eff992d8fb4..f6406d00f87 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -171,4 +171,10 @@ type Post1 { type Comment1 { id: String! @id replies: [Comment1] +} + +type Person1 { + id: ID! + name: String! + friends: [Person1] @hasInverse(field: friends) } \ No newline at end of file diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index 04c66f67513..bd4b27791b2 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -58,6 +58,15 @@ ], "upsert": true }, + { + "predicate": "Person1.name", + "type": "string" + }, + { + "predicate": "Person1.friends", + "type": "uid", + "list": true + }, { "predicate": "Post1.comments", "type": "uid", @@ -430,6 +439,17 @@ ], "name": "People" }, + { + "fields": [ + { + "name": "Person1.name" + }, + { + "name": "Person1.friends" + } + ], + "name": "Person1" + }, { "fields": [ { diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index 41ac10b39e0..fe2675a3244 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -171,4 +171,10 @@ type Post1 { type Comment1 { id: String! @id replies: [Comment1] +} + +type Person1 { + id: ID! + name: String! + friends: [Person1] @hasInverse(field: friends) } \ No newline at end of file diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index ac1fc5fa81f..0b822d2013a 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -140,6 +140,15 @@ "predicate": "Person.name", "type": "string" }, + { + "predicate": "Person1.name", + "type": "string" + }, + { + "predicate": "Person1.friends", + "type": "uid", + "list": true + }, { "predicate": "Post.author", "type": "uid" @@ -483,6 +492,17 @@ ], "name": "Person" }, + { + "fields": [ + { + "name": "Person1.name" + }, + { + "name": "Person1.friends" + } + ], + "name": "Person1" + }, { "fields": [ { diff --git a/graphql/e2e/schema/schema_test.go b/graphql/e2e/schema/schema_test.go index 5a560aa4c96..ae1e741ccec 100644 --- a/graphql/e2e/schema/schema_test.go +++ b/graphql/e2e/schema/schema_test.go @@ -288,6 +288,35 @@ func TestConcurrentSchemaUpdates(t *testing.T) { require.Equal(t, finalGraphQLSchema, resp.GqlSchema[0].Schema) } +// TestIntrospectionQueryAfterDropAll make sure that Introspection query after drop_all doesn't give any internal error +func TestIntrospectionQueryAfterDropAll(t *testing.T) { + // First Do the drop_all operation + dg, err := testutil.DgraphClient(groupOnegRPC) + require.NoError(t, err) + testutil.DropAll(t, dg) + // wait for a bit + time.Sleep(time.Second) + + introspectionQuery := ` + query{ + __schema{ + types{ + name + } + } + }` + introspect := &common.GraphQLParams{ + Query: introspectionQuery, + } + + // On doing Introspection Query Now, We should get the Expected Error Message, not the Internal Error. + introspectionResult := introspect.ExecuteAsPost(t, groupOneServer) + require.Len(t, introspectionResult.Errors, 1) + gotErrorMessage := introspectionResult.Errors[0].Message + expectedErrorMessage := "Not resolving __schema. There's no GraphQL schema in Dgraph. Use the /admin API to add a GraphQL schema" + require.Equal(t, expectedErrorMessage, gotErrorMessage) +} + // TestUpdateGQLSchemaAfterDropAll makes sure that updating the GraphQL schema after drop_all works func TestUpdateGQLSchemaAfterDropAll(t *testing.T) { updateGQLSchemaRequireNoErrors(t, ` @@ -376,6 +405,36 @@ func TestUpdateGQLSchemaFields(t *testing.T) { require.Equal(t, string(generatedSchema), updateResp.UpdateGQLSchema.GQLSchema.GeneratedSchema) } +func TestIntrospection(t *testing.T) { + // note that both the types implement the same interface and have a field called `name`, which + // has exact same name as a field in full introspection query. + schema := ` + interface Node { + id: ID! + } + + type Human implements Node { + name: String + } + + type Dog implements Node { + name: String + }` + updateGQLSchemaRequireNoErrors(t, schema, groupOneAdminServer) + query, err := ioutil.ReadFile("../../schema/testdata/introspection/input/full_query.graphql") + require.NoError(t, err) + + introspectionParams := &common.GraphQLParams{Query: string(query)} + resp := introspectionParams.ExecuteAsPost(t, groupOneServer) + + // checking that there are no errors in the response, i.e., we always get some data in the + // introspection response. + require.Nilf(t, resp.Errors, "%s", resp.Errors) + require.NotEmpty(t, resp.Data) + // TODO: we should actually compare data here, but there seems to be some issue with either the + // introspection response or the JSON comparison. Needs deeper looking. +} + func updateGQLSchema(t *testing.T, schema, url string) *common.GraphQLResponse { req := &common.GraphQLParams{ Query: `mutation updateGQLSchema($sch: String!) { diff --git a/graphql/resolve/add_mutation_test.yaml b/graphql/resolve/add_mutation_test.yaml index a780c466024..91a80162e4f 100644 --- a/graphql/resolve/add_mutation_test.yaml +++ b/graphql/resolve/add_mutation_test.yaml @@ -2413,3 +2413,61 @@ explanation: "The add mutation should not be allowed since value of @id field is empty." error: { "message": "failed to rewrite mutation payload because encountered an empty value for @id field `State.code`" } + +- + name: "Add mutation for person with @hasInverse" + gqlmutation: | + mutation($input: [AddPersonInput!]!) { + addPerson(input: $input) { + person { + name + } + } + } + gqlvariables: | + { + "input": [ + { + "name": "Or", + "friends": [ + { "name": "Michal", "friends": [{ "name": "Justin" }] } + ] + } + ] + } + dgmutations: + - setjson: | + { + "Person.friends": [ + { + "Person.friends": [ + { + "uid": "_:Person1" + }, + { + "Person.friends": [ + { + "uid": "_:Person2" + } + ], + "Person.name": "Justin", + "dgraph.type": [ + "Person" + ], + "uid": "_:Person3" + } + ], + "Person.name": "Michal", + "dgraph.type": [ + "Person" + ], + "uid": "_:Person2" + } + ], + "Person.name": "Or", + "dgraph.type": [ + "Person" + ], + "uid": "_:Person1" + } + diff --git a/graphql/resolve/auth_delete_test.yaml b/graphql/resolve/auth_delete_test.yaml index f8837eb2e2a..1f202cff726 100644 --- a/graphql/resolve/auth_delete_test.yaml +++ b/graphql/resolve/auth_delete_test.yaml @@ -24,6 +24,76 @@ UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } +- name: "Delete with inverse field and RBAC true" + gqlquery: | + mutation { + deleteTweets( + filter: { + text: {anyoftext: "abc"} + }) { + tweets { + text + } + } + } + jwtvar: + USER: "foo" + ROLE: "admin" + dgmutations: + - deletejson: | + [ + { "uid": "uid(x)" }, + { + "User.tweets" : [{"uid":"uid(x)"}], + "uid" : "uid(User2)" + } + ] + dgquery: |- + query { + x as deleteTweets(func: uid(TweetsRoot)) { + uid + User2 as Tweets.user + } + TweetsRoot as var(func: uid(Tweets1)) + Tweets1 as var(func: type(Tweets)) @filter(anyoftext(Tweets.text, "abc")) + tweets(func: uid(Tweets3)) { + text : Tweets.text + dgraph.uid : uid + } + Tweets3 as var(func: uid(Tweets4)) + Tweets4 as var(func: uid(x)) + } + +- name: "Delete with inverse field and RBAC false" + gqlquery: | + mutation { + deleteTweets( + filter: { + text: {anyoftext: "abc"} + }) { + tweets { + text + } + } + } + jwtvar: + ROLE: "admin" + dgmutations: + - deletejson: | + [ + { "uid": "uid(x)" } + ] + dgquery: |- + query { + x as deleteTweets() + tweets(func: uid(Tweets1)) { + text : Tweets.text + dgraph.uid : uid + } + Tweets1 as var(func: uid(Tweets2)) + Tweets2 as var(func: uid(x)) + } + - name: "Delete with deep auth" gqlquery: | mutation deleteTicket($filter: TicketFilter!) { @@ -288,6 +358,10 @@ { "Ticket.assignedTo" : [ {"uid":"uid(x)"} ], "uid" : "uid(Ticket4)" + }, + { + "Tweets.user" : {"uid":"uid(x)"}, + "uid" : "uid(Tweets5)" } ] dgquery: |- @@ -295,6 +369,7 @@ x as deleteUser(func: uid(UserRoot)) { uid Ticket4 as User.tickets + Tweets5 as User.tweets } UserRoot as var(func: uid(User1)) @filter((uid(UserAuth2) AND uid(UserAuth3))) User1 as var(func: type(User)) @filter(eq(User.username, "userxyz")) @@ -395,7 +470,7 @@ random : Log.random dgraph.uid : uid } - Log2 as var(func: uid(Log3), orderasc: Log.logs) + Log2 as var(func: uid(Log3)) Log3 as var(func: uid(x)) } diff --git a/graphql/resolve/auth_query_test.yaml b/graphql/resolve/auth_query_test.yaml index c099f1913c3..2f404272f1a 100644 --- a/graphql/resolve/auth_query_test.yaml +++ b/graphql/resolve/auth_query_test.yaml @@ -7,7 +7,7 @@ adminTasks { id name - occurrances { + occurrences { due comp } @@ -26,9 +26,9 @@ adminTasks : Contact.adminTasks @filter(uid(AdminTask5)) { id : uid name : AdminTask.name - occurrances : AdminTask.occurrances @filter(uid(TaskOccurance4)) { - due : TaskOccurance.due - comp : TaskOccurance.comp + occurrences : AdminTask.occurrences @filter(uid(TaskOccurrence4)) { + due : TaskOccurrence.due + comp : TaskOccurrence.comp dgraph.uid : uid } } @@ -40,10 +40,10 @@ } AdminTask5 as var(func: uid(AdminTask1)) var(func: uid(AdminTask1)) { - TaskOccurance2 as AdminTask.occurrances + TaskOccurrence2 as AdminTask.occurrences } - TaskOccurance4 as var(func: uid(TaskOccurance2)) @filter(uid(TaskOccuranceAuth3)) - TaskOccuranceAuth3 as var(func: uid(TaskOccurance2)) @filter(eq(TaskOccurance.role, "ADMINISTRATOR")) @cascade + TaskOccurrence4 as var(func: uid(TaskOccurrence2)) @filter(uid(TaskOccurrenceAuth3)) + TaskOccurrenceAuth3 as var(func: uid(TaskOccurrence2)) @filter(eq(TaskOccurrence.role, "ADMINISTRATOR")) @cascade } - name: "Deep RBAC rule - Level 0 false" @@ -55,7 +55,7 @@ adminTasks { id name - occurrances { + occurrences { due comp } @@ -80,7 +80,7 @@ adminTasks { id name - occurrances { + occurrences { due comp } @@ -101,6 +101,55 @@ Contact5 as var(func: type(Contact)) } +- name: "Deep RBAC rule with cascade - Level 1 false" + gqlquery: | + query { + queryContact @cascade { + id + nickName + adminTasks { + id + name + occurrences { + due + comp + } + } + } + } + jwtvar: + ContactRole: ADMINISTRATOR + TaskRole: User + TaskOccuranceRole: ADMINISTRATOR + dgquery: |- + query { + queryContact(func: uid(ContactRoot)) @cascade { + id : uid + nickName : Contact.nickName + adminTasks : Contact.adminTasks @filter(uid(AdminTask6)) { + id : uid + name : AdminTask.name + occurrences : AdminTask.occurrences @filter(uid(TaskOccurrence4)) { + due : TaskOccurrence.due + comp : TaskOccurrence.comp + dgraph.uid : uid + } + } + } + ContactRoot as var(func: uid(Contact7)) + Contact7 as var(func: type(Contact)) + var(func: uid(ContactRoot)) { + AdminTask1 as Contact.adminTasks + } + AdminTask6 as var(func: uid(AdminTask1)) @filter(uid(AdminTask5)) + var(func: uid(AdminTask1)) { + TaskOccurrence2 as AdminTask.occurrences + } + TaskOccurrence4 as var(func: uid(TaskOccurrence2)) @filter(uid(TaskOccurrenceAuth3)) + TaskOccurrenceAuth3 as var(func: uid(TaskOccurrence2)) @filter(eq(TaskOccurrence.role, "ADMINISTRATOR")) @cascade + AdminTask5 as var(func: uid()) + } + - name: "Deep RBAC rule - Level 2 false" gqlquery: | query { @@ -110,7 +159,7 @@ adminTasks { id name - occurrances { + occurrences { due comp } @@ -148,7 +197,7 @@ tasks { id name - occurrances { + occurrences { due comp } @@ -167,9 +216,9 @@ tasks : Contact.tasks @filter(uid(Task5)) { id : uid name : Task.name - occurrances : Task.occurrances @filter(uid(TaskOccurance4)) { - due : TaskOccurance.due - comp : TaskOccurance.comp + occurrences : Task.occurrences @filter(uid(TaskOccurrence4)) { + due : TaskOccurrence.due + comp : TaskOccurrence.comp dgraph.uid : uid } } @@ -181,10 +230,10 @@ } Task5 as var(func: uid(Task1)) var(func: uid(Task1)) { - TaskOccurance2 as Task.occurrances + TaskOccurrence2 as Task.occurrences } - TaskOccurance4 as var(func: uid(TaskOccurance2)) @filter(uid(TaskOccuranceAuth3)) - TaskOccuranceAuth3 as var(func: uid(TaskOccurance2)) @filter(eq(TaskOccurance.role, "ADMINISTRATOR")) @cascade + TaskOccurrence4 as var(func: uid(TaskOccurrence2)) @filter(uid(TaskOccurrenceAuth3)) + TaskOccurrenceAuth3 as var(func: uid(TaskOccurrence2)) @filter(eq(TaskOccurrence.role, "ADMINISTRATOR")) @cascade } - name: "Auth query with @dgraph pred." @@ -584,11 +633,11 @@ USER: "user1" dgquery: |- query { - queryUserSecret(func: uid(UserSecretRoot), orderasc: UserSecret.aSecret) { + queryUserSecret(func: uid(UserSecretRoot), orderasc: UserSecret.aSecret, first: 1) { id : uid ownedBy : UserSecret.ownedBy } - UserSecretRoot as var(func: uid(UserSecret1), orderasc: UserSecret.aSecret, first: 1) @filter(uid(UserSecretAuth2)) + UserSecretRoot as var(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) UserSecret1 as var(func: type(UserSecret)) @filter(eq(UserSecret.ownedBy, "user2")) UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } @@ -725,11 +774,11 @@ USER: "user1" dgquery: |- query { - queryMovie(func: uid(MovieRoot), orderasc: Movie.content) { + queryMovie(func: uid(MovieRoot), orderasc: Movie.content, first: 10, offset: 10) { content : Movie.content dgraph.uid : uid } - MovieRoot as var(func: uid(Movie1), orderasc: Movie.content, first: 10, offset: 10) @filter((NOT (uid(MovieAuth2)) AND (uid(MovieAuth3) OR uid(MovieAuth4)))) + MovieRoot as var(func: uid(Movie1)) @filter((NOT (uid(MovieAuth2)) AND (uid(MovieAuth3) OR uid(MovieAuth4)))) Movie1 as var(func: type(Movie)) @filter(eq(Movie.content, "A. N. Author")) MovieAuth2 as var(func: uid(Movie1)) @filter(eq(Movie.hidden, true)) @cascade MovieAuth3 as var(func: uid(Movie1)) @cascade { @@ -760,16 +809,16 @@ USER: "user1" dgquery: |- query { - queryMovie(func: uid(MovieRoot), orderasc: Movie.content) @cascade { + queryMovie(func: uid(MovieRoot), orderasc: Movie.content, first: 10, offset: 10) @cascade { content : Movie.content - regionsAvailable : Movie.regionsAvailable @filter(uid(Region2)) (orderasc: Region.name) { + regionsAvailable : Movie.regionsAvailable @filter(uid(Region2)) (orderasc: Region.name, first: 10, offset: 10) { name : Region.name global : Region.global dgraph.uid : uid } dgraph.uid : uid } - MovieRoot as var(func: uid(Movie3), orderasc: Movie.content, first: 10, offset: 10) @filter((NOT (uid(MovieAuth4)) AND (uid(MovieAuth5) OR uid(MovieAuth6)))) + MovieRoot as var(func: uid(Movie3)) @filter((NOT (uid(MovieAuth4)) AND (uid(MovieAuth5) OR uid(MovieAuth6)))) Movie3 as var(func: type(Movie)) @filter(eq(Movie.content, "MovieXYZ")) MovieAuth4 as var(func: uid(Movie3)) @filter(eq(Movie.hidden, true)) @cascade MovieAuth5 as var(func: uid(Movie3)) @cascade { @@ -786,7 +835,7 @@ var(func: uid(MovieRoot)) { Region1 as Movie.regionsAvailable } - Region2 as var(func: uid(Region1), orderasc: Region.name, first: 10, offset: 10) @filter(eq(Region.name, "Region123")) + Region2 as var(func: uid(Region1)) @filter(eq(Region.name, "Region123")) } - name: "Auth deep query - 3 level" @@ -813,16 +862,16 @@ USER: "user1" dgquery: |- query { - queryMovie(func: uid(MovieRoot), orderasc: Movie.content) { + queryMovie(func: uid(MovieRoot), orderasc: Movie.content, first: 10, offset: 10) { content : Movie.content - regionsAvailable : Movie.regionsAvailable @filter(uid(Region7)) (orderasc: Region.name) @cascade { + regionsAvailable : Movie.regionsAvailable @filter(uid(Region7)) (orderasc: Region.name, first: 10, offset: 10) @cascade { name : Region.name global : Region.global - users : Region.users @filter(uid(User6)) (orderasc: User.username) { + users : Region.users @filter(uid(User6)) (orderasc: User.username, first: 10, offset: 10) { username : User.username age : User.age isPublic : User.isPublic - secrets : User.secrets @filter(uid(UserSecret5)) (orderasc: UserSecret.aSecret) { + secrets : User.secrets @filter(uid(UserSecret5)) (orderasc: UserSecret.aSecret, first: 10, offset: 10) { aSecret : UserSecret.aSecret ownedBy : UserSecret.ownedBy dgraph.uid : uid @@ -833,7 +882,7 @@ } dgraph.uid : uid } - MovieRoot as var(func: uid(Movie8), orderasc: Movie.content, first: 10, offset: 10) @filter((NOT (uid(MovieAuth9)) AND (uid(MovieAuth10) OR uid(MovieAuth11)))) + MovieRoot as var(func: uid(Movie8)) @filter((NOT (uid(MovieAuth9)) AND (uid(MovieAuth10) OR uid(MovieAuth11)))) Movie8 as var(func: type(Movie)) @filter(eq(Movie.content, "MovieXYZ")) MovieAuth9 as var(func: uid(Movie8)) @filter(eq(Movie.hidden, true)) @cascade MovieAuth10 as var(func: uid(Movie8)) @cascade { @@ -850,15 +899,15 @@ var(func: uid(MovieRoot)) { Region1 as Movie.regionsAvailable } - Region7 as var(func: uid(Region1), orderasc: Region.name, first: 10, offset: 10) @filter(eq(Region.name, "Region123")) + Region7 as var(func: uid(Region1)) @filter(eq(Region.name, "Region123")) var(func: uid(Region1)) { User2 as Region.users } - User6 as var(func: uid(User2), orderasc: User.username, first: 10, offset: 10) @filter(eq(User.username, "User321")) + User6 as var(func: uid(User2)) @filter(eq(User.username, "User321")) var(func: uid(User2)) { UserSecret3 as User.secrets } - UserSecret5 as var(func: uid(UserSecret3), orderasc: UserSecret.aSecret, first: 10, offset: 10) @filter((allofterms(UserSecret.aSecret, "Secret132") AND uid(UserSecretAuth4))) + UserSecret5 as var(func: uid(UserSecret3)) @filter((allofterms(UserSecret.aSecret, "Secret132") AND uid(UserSecretAuth4))) UserSecretAuth4 as var(func: uid(UserSecret3)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } diff --git a/graphql/resolve/delete_mutation_test.yaml b/graphql/resolve/delete_mutation_test.yaml index b2e970b9ad8..bd884725eec 100644 --- a/graphql/resolve/delete_mutation_test.yaml +++ b/graphql/resolve/delete_mutation_test.yaml @@ -265,4 +265,30 @@ dgquery: |- query { x as deleteX() - } \ No newline at end of file + } + +- + name: "Deleting an interface with just a field with @id directive" + gqlmutation: | + mutation{ + deleteA(filter:{name:{eq: "xyz"}}){ + a{ + name + } + } + } + dgquery: |- + query { + x as deleteA(func: type(A)) @filter(eq(A.name, "xyz")) { + uid + } + a(func: uid(x)) { + dgraph.type + name : A.name + dgraph.uid : uid + } + } + dgmutations: + - deletejson: | + [{ "uid": "uid(x)"}] + diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index 0106948a916..90050d51ae7 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -670,6 +670,43 @@ func RewriteUpsertQueryFromMutation(m schema.Mutation, authRw *authRewriter) *gq return dgQuery } +// removeNodeReference removes any reference we know about (via @hasInverse) into a node. +func removeNodeReference(m schema.Mutation, authRw *authRewriter, + qry *gql.GraphQuery) []interface{} { + var deletes []interface{} + for _, fld := range m.MutatedType().Fields() { + invField := fld.Inverse() + if invField == nil { + // This field be a reverse edge, in that case we need to delete the incoming connections + // to this node via its forward edges. + invField = fld.ForwardEdge() + if invField == nil { + continue + } + } + varName := authRw.varGen.Next(fld.Type(), "", "", false) + + qry.Children = append(qry.Children, + &gql.GraphQuery{ + Var: varName, + Attr: invField.Type().DgraphPredicate(fld.Name()), + }) + + delFldName := fld.Type().DgraphPredicate(invField.Name()) + del := map[string]interface{}{"uid": MutationQueryVarUID} + if invField.Type().ListType() == nil { + deletes = append(deletes, map[string]interface{}{ + "uid": fmt.Sprintf("uid(%s)", varName), + delFldName: del}) + } else { + deletes = append(deletes, map[string]interface{}{ + "uid": fmt.Sprintf("uid(%s)", varName), + delFldName: []interface{}{del}}) + } + } + return deletes +} + func (drw *deleteRewriter) Rewrite( ctx context.Context, m schema.Mutation) ([]*UpsertMutation, error) { @@ -703,44 +740,14 @@ func (drw *deleteRewriter) Rewrite( } deletes := []interface{}{map[string]interface{}{"uid": "uid(x)"}} - - // we need to delete this node with ^^ and then any reference we know about - // (via @hasInverse) into this node. - for _, fld := range m.MutatedType().Fields() { - invField := fld.Inverse() - if invField == nil { - // This field be a reverse edge, in that case we need to delete the incoming connections - // to this node via its forward edges. - invField = fld.ForwardEdge() - if invField == nil { - continue - } - } - varName := varGen.Next(fld.Type(), "", "", false) - - qry.Children = append(qry.Children, - &gql.GraphQuery{ - Var: varName, - Attr: invField.Type().DgraphPredicate(fld.Name()), - }) - - delFldName := fld.Type().DgraphPredicate(invField.Name()) - del := map[string]interface{}{"uid": MutationQueryVarUID} - if invField.Type().ListType() == nil { - deletes = append(deletes, - map[string]interface{}{ - "uid": fmt.Sprintf("uid(%s)", varName), - delFldName: del}) - } else { - deletes = append(deletes, - map[string]interface{}{ - "uid": fmt.Sprintf("uid(%s)", varName), - delFldName: []interface{}{del}}) - } + // We need to remove node reference only if auth rule succeeds. + if qry.Attr != m.ResponseName()+"()" { + // We need to delete the node and then any reference we know about (via @hasInverse) + // into this node. + deletes = append(deletes, removeNodeReference(m, authRw, qry)...) } b, err := json.Marshal(deletes) - var finalQry *gql.GraphQuery // This rewrites the Upsert mutation so we can query the nodes before deletion. The query result // is later added to delete mutation result. @@ -1088,9 +1095,17 @@ func rewriteObject( xidMetadata.queryExists[variable] = true } frag.conditions = []string{fmt.Sprintf("eq(len(%s), 0)", variable)} - frag.check = checkQueryResult(variable, - x.GqlErrorf("id %s already exists for type %s", xidString, typ.Name()), - nil) + + // We need to conceal the error because we might be leaking information to the user if it + // tries to add duplicate data to the field with @id. + var err error + if queryAuthSelector(typ) == nil { + err = x.GqlErrorf("id %s already exists for type %s", xidString, typ.Name()) + } else { + // This error will only be reported in debug mode. + err = x.GqlErrorf("GraphQL debug: id already exists for type %s", typ.Name()) + } + frag.check = checkQueryResult(variable, err, nil) } if xid != nil && !atTopLevel { @@ -1720,7 +1735,27 @@ func squashIntoObject(label string) func(interface{}, interface{}, bool) interfa } asObject = cpy } - asObject[label] = v + + val := v + + // If there is an existing value for the label in the object, then we should append to it + // instead of overwriting it if the existing value is a list. This can happen when there + // is @hasInverse and we are doing nested adds. + existing := asObject[label] + switch ev := existing.(type) { + case []interface{}: + switch vv := v.(type) { + case []interface{}: + ev = append(ev, vv...) + val = ev + case interface{}: + ev = append(ev, vv) + val = ev + default: + } + default: + } + asObject[label] = val return asObject } } diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index d659636f4f2..a68f2678ad0 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -46,6 +46,8 @@ type authRewriter struct { parentVarName string // `hasAuthRules` indicates if any of fields in the complete query hierarchy has auth rules. hasAuthRules bool + // `hasCascade` indicates if any of fields in the complete query hierarchy has cascade directive. + hasCascade bool } // NewQueryRewriter returns a new QueryRewriter. @@ -67,6 +69,19 @@ func hasAuthRules(field schema.Field, authRw *authRewriter) bool { return false } +func hasCascadeDirective(field schema.Field) bool { + if c := field.Cascade(); c { + return true + } + + for _, childField := range field.SelectionSet() { + if res := hasCascadeDirective(childField); res { + return true + } + } + return false +} + // Rewrite rewrites a GraphQL query into a Dgraph GraphQuery. func (qr *queryRewriter) Rewrite( ctx context.Context, @@ -93,13 +108,14 @@ func (qr *queryRewriter) Rewrite( parentVarName: gqlQuery.Type().Name() + "Root", } authRw.hasAuthRules = hasAuthRules(gqlQuery, authRw) + authRw.hasCascade = hasCascadeDirective(gqlQuery) switch gqlQuery.QueryType() { case schema.GetQuery: // TODO: The only error that can occur in query rewriting is if an ID argument // can't be parsed as a uid: e.g. the query was something like: - // + //UserSecret // getT(id: "HI") { ... } // // But that's not a rewriting error! It should be caught by validation @@ -268,6 +284,11 @@ func addArgumentsToField(dgQuery *gql.GraphQuery, field schema.Field) { addPagination(dgQuery, field) } +func addFilterToField(dgQuery *gql.GraphQuery, field schema.Field) { + filter, _ := field.ArgValue("filter").(map[string]interface{}) + addFilter(dgQuery, field.Type(), filter) +} + func addTopLevelTypeFilter(query *gql.GraphQuery, field schema.Field) { if query.Attr != "" { addTypeFilter(query, field.Type()) @@ -453,12 +474,9 @@ func (authRw *authRewriter) addAuthQueries( Args: []gql.Arg{{Value: authRw.varName}}, }, Filter: filter, - Order: dgQuery.Order, - Args: dgQuery.Args, } dgQuery.Filter = nil - dgQuery.Args = nil // The user query starts from the var query generated above and is filtered // by the the filter generated from auth processing, so now we build @@ -746,14 +764,34 @@ func addSelectionSetFrom( q.Children = append(q.Children, child) } - // If RBAC rules are evaluated to Negative, we don't write queries for deeper levels. - // Hence we don't need to do any further processing for this field. - if rbac == schema.Negative { + var fieldAuth []*gql.GraphQuery + var authFilter *gql.FilterTree + if rbac == schema.Negative && auth.hasAuthRules && auth.hasCascade && !auth.isWritingAuth { + // If RBAC rules are evaluated to Negative but we have cascade directive we continue + // to write the query and add a dummy filter that doesn't return anything. + // Example: AdminTask5 as var(func: uid()) + q.Children = append(q.Children, child) + varName := auth.varGen.Next(f.Type(), "", "", auth.isWritingAuth) + fieldAuth = append(fieldAuth, &gql.GraphQuery{ + Var: varName, + Attr: "var", + Func: &gql.Function{ + Name: "uid", + }, + }) + authFilter = &gql.FilterTree{ + Func: &gql.Function{ + Name: "uid", + Args: []gql.Arg{{Value: varName}}, + }, + } + rbac = schema.Positive + } else if rbac == schema.Negative { + // If RBAC rules are evaluated to Negative, we don't write queries for deeper levels. + // Hence we don't need to do any further processing for this field. continue } - var fieldAuth []*gql.GraphQuery - var authFilter *gql.FilterTree // If RBAC rules are evaluated to `Uncertain` then we add the Auth rules. if rbac == schema.Uncertain { fieldAuth, authFilter = auth.rewriteAuthQueries(f.Type()) @@ -799,7 +837,7 @@ func addSelectionSetFrom( }, } - addArgumentsToField(selectionQry, f) + addFilterToField(selectionQry, f) selectionQry.Filter = child.Filter authQueries = append(authQueries, parentQry, selectionQry) child.Filter = &gql.FilterTree{ @@ -808,10 +846,6 @@ func addSelectionSetFrom( Args: []gql.Arg{{Value: filtervarName}}, }, } - - // We already apply the following to `selectionQry` by calling addArgumentsToField() - // hence they are no longer required. - child.Args = nil } authQueries = append(authQueries, selectionAuth...) authQueries = append(authQueries, fieldAuth...) diff --git a/graphql/resolve/schema.graphql b/graphql/resolve/schema.graphql index eef8ed55b4e..5d4a305af30 100644 --- a/graphql/resolve/schema.graphql +++ b/graphql/resolve/schema.graphql @@ -282,3 +282,12 @@ type ThingTwo implements Thing { prop: String @dgraph(pred: "prop") owner: String } + +type Person { + id: ID! + name: String @search(by: [hash]) + friends: [Person] @hasInverse(field: friends) +} +interface A { + name: String! @id +} \ No newline at end of file diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index 96817e8cad2..ecb78d8b1df 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -551,11 +551,88 @@ func completeSchema(sch *ast.Schema, definitions []string) { } } +func cleanupInput(sch *ast.Schema, def *ast.Definition, seen map[string]bool) { + // seen helps us avoid cycles + if def == nil || seen[def.Name] { + return + } + + i := 0 + // recursively walk over fields for an input type and delete those which are themselves empty. + for _, f := range def.Fields { + nt := f.Type.Name() + enum := sch.Types[nt] != nil && sch.Types[nt].Kind == "ENUM" + // Lets skip scalar types and enums. + if _, ok := scalarToDgraph[nt]; ok || enum { + def.Fields[i] = f + i++ + continue + } + + seen[def.Name] = true + cleanupInput(sch, sch.Types[nt], seen) + seen[def.Name] = false + + // If after calling cleanup on an input type, it got deleted then it doesn't need to be + // in the fields for this type anymore. + if sch.Types[nt] == nil { + continue + } + def.Fields[i] = f + i++ + } + def.Fields = def.Fields[:i] + + if len(def.Fields) == 0 { + delete(sch.Types, def.Name) + } +} + +func cleanSchema(sch *ast.Schema) { + // Let's go over inputs of the type TRef, TPatch and AddTInput and delete the ones which + // don't have field inside them. + for k := range sch.Types { + if strings.HasSuffix(k, "Ref") || strings.HasSuffix(k, "Patch") || + (strings.HasPrefix(k, "Add") && strings.HasSuffix(k, "Input")) { + cleanupInput(sch, sch.Types[k], map[string]bool{}) + } + } + + // Let's go over mutations and cleanup those which don't have AddTInput defined in the schema + // anymore. + i := 0 // helps us overwrite the array with valid entries. + for _, field := range sch.Mutation.Fields { + custom := field.Directives.ForName("custom") + // We would only modify add type queries. + if custom != nil || !strings.HasPrefix(field.Name, "add") { + sch.Mutation.Fields[i] = field + i++ + continue + } + + // addT type mutations have an input which is AddTInput so if that doesn't exist anymore, + // we can delete the AddTPayload and also skip this mutation. + typ := field.Name[3:] + input := sch.Types["Add"+typ+"Input"] + if input == nil { + delete(sch.Types, "Add"+typ+"Payload") + continue + } + sch.Mutation.Fields[i] = field + i++ + + } + sch.Mutation.Fields = sch.Mutation.Fields[:i] +} + func addInputType(schema *ast.Schema, defn *ast.Definition) { - schema.Types["Add"+defn.Name+"Input"] = &ast.Definition{ - Kind: ast.InputObject, - Name: "Add" + defn.Name + "Input", - Fields: getFieldsWithoutIDType(schema, defn), + field := getFieldsWithoutIDType(schema, defn) + if len(field) != 0 { + schema.Types["Add"+defn.Name+"Input"] = &ast.Definition{ + Kind: ast.InputObject, + Name: "Add" + defn.Name + "Input", + Fields: field, + } } } @@ -578,10 +655,12 @@ func addReferenceType(schema *ast.Schema, defn *ast.Definition) { } } - schema.Types[defn.Name+"Ref"] = &ast.Definition{ - Kind: ast.InputObject, - Name: defn.Name + "Ref", - Fields: flds, + if len(flds) != 0 { + schema.Types[defn.Name+"Ref"] = &ast.Definition{ + Kind: ast.InputObject, + Name: defn.Name + "Ref", + Fields: flds, + } } } @@ -835,7 +914,11 @@ func hasFilterable(defn *ast.Definition) bool { func hasOrderables(defn *ast.Definition) bool { return fieldAny(defn.Fields, - func(fld *ast.FieldDefinition) bool { return orderable[fld.Type.Name()] }) + func(fld *ast.FieldDefinition) bool { + // lists can't be ordered and NamedType will be empty for lists, + // so it will return false for list fields + return orderable[fld.Type.NamedType] + }) } func hasID(defn *ast.Definition) bool { @@ -956,7 +1039,7 @@ func addTypeOrderable(schema *ast.Schema, defn *ast.Definition) { } for _, fld := range defn.Fields { - if orderable[fld.Type.Name()] { + if fld.Type.NamedType != "" && orderable[fld.Type.NamedType] { order.EnumValues = append(order.EnumValues, &ast.EnumValueDefinition{Name: fld.Name}) } @@ -976,11 +1059,12 @@ func addAddPayloadType(schema *ast.Schema, defn *ast.Definition) { addFilterArgument(schema, qry) addOrderArgument(schema, qry) addPaginationArguments(qry) - - schema.Types["Add"+defn.Name+"Payload"] = &ast.Definition{ - Kind: ast.Object, - Name: "Add" + defn.Name + "Payload", - Fields: []*ast.FieldDefinition{qry, numUids}, + if schema.Types["Add"+defn.Name+"Input"] != nil { + schema.Types["Add"+defn.Name+"Payload"] = &ast.Definition{ + Kind: ast.Object, + Name: "Add" + defn.Name + "Payload", + Fields: []*ast.FieldDefinition{qry, numUids}, + } } } @@ -1171,6 +1255,7 @@ func addAddMutation(schema *ast.Schema, defn *ast.Definition) { }, } schema.Mutation.Fields = append(schema.Mutation.Fields, add) + } func addUpdateMutation(schema *ast.Schema, defn *ast.Definition) { @@ -1221,6 +1306,9 @@ func addDeleteMutation(schema *ast.Schema, defn *ast.Definition) { } func addMutations(schema *ast.Schema, defn *ast.Definition) { + if schema.Types["Add"+defn.Name+"Input"] == nil { + return + } addAddMutation(schema, defn) addUpdateMutation(schema, defn) addDeleteMutation(schema, defn) @@ -1321,7 +1409,6 @@ func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition) ast.FieldL (!hasID(schema.Types[fld.Type.Name()]) && !hasXID(schema.Types[fld.Type.Name()])) { continue } - fldList = append(fldList, createField(schema, fld)) } diff --git a/graphql/schema/introspection_test.go b/graphql/schema/introspection_test.go index ebad506fc55..0595e52e462 100644 --- a/graphql/schema/introspection_test.go +++ b/graphql/schema/introspection_test.go @@ -221,122 +221,6 @@ func TestIntrospectionQueryWithVars(t *testing.T) { testutil.CompareJSON(t, string(expectedBuf), string(resp)) } -const ( - testIntrospectionQuery = `query { - __schema { - __typename - queryType { - name - __typename - } - mutationType { - name - __typename - } - subscriptionType { - name - __typename - } - types { - ...FullType - } - directives { - __typename - name - locations - args { - ...InputValue - } - } - } - } - fragment FullType on __Type { - kind - name - fields(includeDeprecated: true) { - __typename - name - args { - ...InputValue - __typename - } - type { - ...TypeRef - __typename - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - __typename - } - interfaces { - ...TypeRef - __typename - } - enumValues(includeDeprecated: true) { - name - isDeprecated - deprecationReason - __typename - } - possibleTypes { - ...TypeRef - __typename - } - __typename - } - fragment InputValue on __InputValue { - __typename - name - type { - ...TypeRef - } - defaultValue - } - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - __typename - } - __typename - } - __typename - } - __typename - } - __typename - } - __typename - } - __typename - } - __typename - }` -) - func TestFullIntrospectionQuery(t *testing.T) { sch := gqlparser.MustLoadSchema( &ast.Source{Name: "schema.graphql", Input: ` @@ -349,7 +233,10 @@ func TestFullIntrospectionQuery(t *testing.T) { } `}) - doc, gqlErr := parser.ParseQuery(&ast.Source{Input: testIntrospectionQuery}) + fullIntrospectionQuery, err := ioutil.ReadFile("testdata/introspection/input/full_query.graphql") + require.NoError(t, err) + + doc, gqlErr := parser.ParseQuery(&ast.Source{Input: string(fullIntrospectionQuery)}) require.Nil(t, gqlErr) listErr := validator.Validate(sch, doc) @@ -359,7 +246,7 @@ func TestFullIntrospectionQuery(t *testing.T) { require.NotNil(t, op) oper := &operation{op: op, vars: map[string]interface{}{}, - query: string(testIntrospectionQuery), + query: string(fullIntrospectionQuery), doc: doc, inSchema: &schema{schema: sch}, } diff --git a/graphql/schema/response.go b/graphql/schema/response.go index 418ca925a59..06c969ff562 100644 --- a/graphql/schema/response.go +++ b/graphql/schema/response.go @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" "io" + "strings" "time" "github.com/golang/glog" @@ -67,6 +68,17 @@ func (r *Response) GetExtensions() *Extensions { // WithError generates GraphQL errors from err and records those in r. func (r *Response) WithError(err error) { + if err == nil { + return + } + if !x.Config.GraphqlDebug && strings.Contains(err.Error(), "authorization failed") { + return + } + + if !x.Config.GraphqlDebug && strings.Contains(err.Error(), "GraphQL debug:") { + return + } + r.Errors = append(r.Errors, AsGQLErrors(err)...) } diff --git a/graphql/schema/schemagen.go b/graphql/schema/schemagen.go index c6bbe7ebb4b..dc42c2fb86a 100644 --- a/graphql/schema/schemagen.go +++ b/graphql/schema/schemagen.go @@ -204,6 +204,7 @@ func NewHandler(input string) (Handler, error) { headers := getAllowedHeaders(sch, defns) dgSchema := genDgSchema(sch, typesToComplete) completeSchema(sch, typesToComplete) + cleanSchema(sch) if len(sch.Query.Fields) == 0 && len(sch.Mutation.Fields) == 0 { return nil, gqlerror.Errorf("No query or mutation found in the generated schema") diff --git a/graphql/schema/schemagen_test.go b/graphql/schema/schemagen_test.go index 8c6dd0e0ece..dcf96e1e027 100644 --- a/graphql/schema/schemagen_test.go +++ b/graphql/schema/schemagen_test.go @@ -87,7 +87,6 @@ func TestSchemaString(t *testing.T) { _, err = FromString(newSchemaStr) require.NoError(t, err) - outputFileName := outputDir + testFile.Name() str2, err := ioutil.ReadFile(outputFileName) require.NoError(t, err) diff --git a/graphql/schema/testdata/introspection/input/full_query.graphql b/graphql/schema/testdata/introspection/input/full_query.graphql new file mode 100644 index 00000000000..f3b63fe0ef9 --- /dev/null +++ b/graphql/schema/testdata/introspection/input/full_query.graphql @@ -0,0 +1,113 @@ +query { + __schema { + __typename + queryType { + name + __typename + } + mutationType { + name + __typename + } + subscriptionType { + name + __typename + } + types { + ...FullType + } + directives { + __typename + name + locations + args { + ...InputValue + } + } + } +} +fragment FullType on __Type { + kind + name + fields(includeDeprecated: true) { + __typename + name + args { + ...InputValue + __typename + } + type { + ...TypeRef + __typename + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + __typename + } + interfaces { + ...TypeRef + __typename + } + enumValues(includeDeprecated: true) { + name + isDeprecated + deprecationReason + __typename + } + possibleTypes { + ...TypeRef + __typename + } + __typename +} +fragment InputValue on __InputValue { + __typename + name + type { + ...TypeRef + } + defaultValue +} +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + __typename + } + __typename + } + __typename + } + __typename + } + __typename + } + __typename + } + __typename + } + __typename +} \ No newline at end of file diff --git a/graphql/schema/testdata/schemagen/input/filter-cleanSchema-all-empty.graphql b/graphql/schema/testdata/schemagen/input/filter-cleanSchema-all-empty.graphql new file mode 100644 index 00000000000..9c489b6aa88 --- /dev/null +++ b/graphql/schema/testdata/schemagen/input/filter-cleanSchema-all-empty.graphql @@ -0,0 +1,12 @@ +type X { + name: [Y] + f1: [Y] @dgraph(pred: "f1") +} + +type Y { + f1: [X] @dgraph(pred: "~f1") +} + +type Z { + add:[X] +} diff --git a/graphql/schema/testdata/schemagen/input/filter-cleanSchema-circular.graphql b/graphql/schema/testdata/schemagen/input/filter-cleanSchema-circular.graphql new file mode 100644 index 00000000000..560f7b855da --- /dev/null +++ b/graphql/schema/testdata/schemagen/input/filter-cleanSchema-circular.graphql @@ -0,0 +1,14 @@ +type X{ + f1: [Y] @dgraph(pred: "f1") + f3: [Z] @dgraph(pred: "~f3") +} + +type Y{ + f1: [X] @dgraph(pred: "~f1") + f2: [Z] @dgraph(pred: "f2") +} + +type Z{ + f2: [Y] @dgraph(pred: "~f2") + f3: [X] @dgraph(pred: "f3") +} \ No newline at end of file diff --git a/graphql/schema/testdata/schemagen/input/filter-cleanSchema-custom-mutation.graphql b/graphql/schema/testdata/schemagen/input/filter-cleanSchema-custom-mutation.graphql new file mode 100644 index 00000000000..c3704e34625 --- /dev/null +++ b/graphql/schema/testdata/schemagen/input/filter-cleanSchema-custom-mutation.graphql @@ -0,0 +1,16 @@ +type User { + id: ID! + name: String! +} + +input UserInput { + name: String! +} + +type Mutation { + addMyFavouriteUsers(input: [UserInput!]!): [User] @custom(http: { + url: "http://my-api.com", + method: "POST", + body: "{ data: $input }" + }) +} \ No newline at end of file diff --git a/graphql/schema/testdata/schemagen/input/filter-cleanSchema-directLink.graphql b/graphql/schema/testdata/schemagen/input/filter-cleanSchema-directLink.graphql new file mode 100644 index 00000000000..e66f9a3cf86 --- /dev/null +++ b/graphql/schema/testdata/schemagen/input/filter-cleanSchema-directLink.graphql @@ -0,0 +1,14 @@ +type X { + f1: [Y] @dgraph(pred: "f1") + name: String + id: ID +} + +type Y { + f2: [Z] @dgraph(pred: "~f2") + f1: [X] @dgraph(pred: "~f1") +} + +type Z { + f2: [Y] @dgraph(pred: "f2") +} \ No newline at end of file diff --git a/graphql/schema/testdata/schemagen/input/hasInverse-with-interface-having-directive.graphql b/graphql/schema/testdata/schemagen/input/hasInverse-with-interface-having-directive.graphql index e0d8dcd006b..3f2c1edc3ae 100644 --- a/graphql/schema/testdata/schemagen/input/hasInverse-with-interface-having-directive.graphql +++ b/graphql/schema/testdata/schemagen/input/hasInverse-with-interface-having-directive.graphql @@ -1,7 +1,7 @@ type Author { id: ID! name: String! @search(by: [hash]) - posts: [Post] + posts: [Post] } interface Post { @@ -12,7 +12,7 @@ interface Post { } type Question implements Post { - answered: Boolean + answered: Boolean } type Answer implements Post { diff --git a/graphql/schema/testdata/schemagen/input/type-without-orderables.graphql b/graphql/schema/testdata/schemagen/input/type-without-orderables.graphql new file mode 100644 index 00000000000..773242e89f6 --- /dev/null +++ b/graphql/schema/testdata/schemagen/input/type-without-orderables.graphql @@ -0,0 +1,6 @@ +type Data { + id: ID! + intList: [Int] + stringList: [String] + metaData: Data +} \ No newline at end of file diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql new file mode 100644 index 00000000000..8d4875b03b9 --- /dev/null +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql @@ -0,0 +1,146 @@ +####################### +# Input Schema +####################### + +type X { + name(first: Int, offset: Int): [Y] + f1(first: Int, offset: Int): [Y] @dgraph(pred: "f1") +} + +type Y { + f1(first: Int, offset: Int): [X] @dgraph(pred: "~f1") +} + +type Z { + add(first: Int, offset: Int): [X] +} + +####################### +# Extended Definitions +####################### + +scalar DateTime + +enum DgraphIndex { + int + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour +} + +input AuthRule { + and: [AuthRule] + or: [AuthRule] + not: AuthRule + rule: String +} + +enum HTTPMethod { + GET + POST + PUT + PATCH + DELETE +} + +enum Mode { + BATCH + SINGLE +} + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +directive @hasInverse(field: String!) on FIELD_DEFINITION +directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION +directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION +directive @id on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete:AuthRule) on OBJECT +directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE +directive @cascade on FIELD + +input IntFilter { + eq: Int + le: Int + lt: Int + ge: Int + gt: Int +} + +input FloatFilter { + eq: Float + le: Float + lt: Float + ge: Float + gt: Float +} + +input DateTimeFilter { + eq: DateTime + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime +} + +input StringTermFilter { + allofterms: String + anyofterms: String +} + +input StringRegExpFilter { + regexp: String +} + +input StringFullTextFilter { + alloftext: String + anyoftext: String +} + +input StringExactFilter { + eq: String + le: String + lt: String + ge: String + gt: String +} + +input StringHashFilter { + eq: String +} + +####################### +# Generated Query +####################### + +type Query { + queryX(first: Int, offset: Int): [X] + queryY(first: Int, offset: Int): [Y] + queryZ(first: Int, offset: Int): [Z] +} + diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql new file mode 100644 index 00000000000..c5e6afee19e --- /dev/null +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql @@ -0,0 +1,205 @@ +####################### +# Input Schema +####################### + +type X { + f1(first: Int, offset: Int): [Y] @dgraph(pred: "f1") + f3(first: Int, offset: Int): [Z] @dgraph(pred: "~f3") +} + +type Y { + f1(first: Int, offset: Int): [X] @dgraph(pred: "~f1") + f2(first: Int, offset: Int): [Z] @dgraph(pred: "f2") +} + +type Z { + f2(first: Int, offset: Int): [Y] @dgraph(pred: "~f2") + f3(first: Int, offset: Int): [X] @dgraph(pred: "f3") +} + +####################### +# Extended Definitions +####################### + +scalar DateTime + +enum DgraphIndex { + int + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour +} + +input AuthRule { + and: [AuthRule] + or: [AuthRule] + not: AuthRule + rule: String +} + +enum HTTPMethod { + GET + POST + PUT + PATCH + DELETE +} + +enum Mode { + BATCH + SINGLE +} + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +directive @hasInverse(field: String!) on FIELD_DEFINITION +directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION +directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION +directive @id on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete:AuthRule) on OBJECT +directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE +directive @cascade on FIELD + +input IntFilter { + eq: Int + le: Int + lt: Int + ge: Int + gt: Int +} + +input FloatFilter { + eq: Float + le: Float + lt: Float + ge: Float + gt: Float +} + +input DateTimeFilter { + eq: DateTime + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime +} + +input StringTermFilter { + allofterms: String + anyofterms: String +} + +input StringRegExpFilter { + regexp: String +} + +input StringFullTextFilter { + alloftext: String + anyoftext: String +} + +input StringExactFilter { + eq: String + le: String + lt: String + ge: String + gt: String +} + +input StringHashFilter { + eq: String +} + +####################### +# Generated Types +####################### + +type AddXPayload { + x(first: Int, offset: Int): [X] + numUids: Int +} + +type AddYPayload { + y(first: Int, offset: Int): [Y] + numUids: Int +} + +type AddZPayload { + z(first: Int, offset: Int): [Z] + numUids: Int +} + +####################### +# Generated Inputs +####################### + +input AddXInput { + f1: [YRef] +} + +input AddYInput { + f2: [ZRef] +} + +input AddZInput { + f3: [XRef] +} + +input XRef { + f1: [YRef] +} + +input YRef { + f2: [ZRef] +} + +input ZRef { + f3: [XRef] +} + +####################### +# Generated Query +####################### + +type Query { + queryX(first: Int, offset: Int): [X] + queryY(first: Int, offset: Int): [Y] + queryZ(first: Int, offset: Int): [Z] +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addX(input: [AddXInput!]!): AddXPayload + addY(input: [AddYInput!]!): AddYPayload + addZ(input: [AddZInput!]!): AddZPayload +} + diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql new file mode 100644 index 00000000000..c5d1c6c5251 --- /dev/null +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql @@ -0,0 +1,214 @@ +####################### +# Input Schema +####################### + +type User { + id: ID! + name: String! +} + +input UserInput { + name: String! +} + +####################### +# Extended Definitions +####################### + +scalar DateTime + +enum DgraphIndex { + int + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour +} + +input AuthRule { + and: [AuthRule] + or: [AuthRule] + not: AuthRule + rule: String +} + +enum HTTPMethod { + GET + POST + PUT + PATCH + DELETE +} + +enum Mode { + BATCH + SINGLE +} + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +directive @hasInverse(field: String!) on FIELD_DEFINITION +directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION +directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION +directive @id on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete:AuthRule) on OBJECT +directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE +directive @cascade on FIELD + +input IntFilter { + eq: Int + le: Int + lt: Int + ge: Int + gt: Int +} + +input FloatFilter { + eq: Float + le: Float + lt: Float + ge: Float + gt: Float +} + +input DateTimeFilter { + eq: DateTime + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime +} + +input StringTermFilter { + allofterms: String + anyofterms: String +} + +input StringRegExpFilter { + regexp: String +} + +input StringFullTextFilter { + alloftext: String + anyoftext: String +} + +input StringExactFilter { + eq: String + le: String + lt: String + ge: String + gt: String +} + +input StringHashFilter { + eq: String +} + +####################### +# Generated Types +####################### + +type AddUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + numUids: Int +} + +type DeleteUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + msg: String + numUids: Int +} + +type UpdateUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + numUids: Int +} + +####################### +# Generated Enums +####################### + +enum UserOrderable { + name +} + +####################### +# Generated Inputs +####################### + +input AddUserInput { + name: String! +} + +input UpdateUserInput { + filter: UserFilter! + set: UserPatch + remove: UserPatch +} + +input UserFilter { + id: [ID!] + not: UserFilter +} + +input UserOrder { + asc: UserOrderable + desc: UserOrderable + then: UserOrder +} + +input UserPatch { + name: String +} + +input UserRef { + id: ID + name: String +} + +####################### +# Generated Query +####################### + +type Query { + getUser(id: ID!): User + queryUser(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addMyFavouriteUsers(input: [UserInput!]!): [User] @custom(http: {url:"http://my-api.com",method:"POST",body:"{ data: $input }"}) + addUser(input: [AddUserInput!]!): AddUserPayload + updateUser(input: UpdateUserInput!): UpdateUserPayload + deleteUser(filter: UserFilter!): DeleteUserPayload +} + diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql new file mode 100644 index 00000000000..9a21c99c142 --- /dev/null +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql @@ -0,0 +1,221 @@ +####################### +# Input Schema +####################### + +type X { + f1(first: Int, offset: Int): [Y] @dgraph(pred: "f1") + name: String + id: ID +} + +type Y { + f2(first: Int, offset: Int): [Z] @dgraph(pred: "~f2") + f1(filter: XFilter, order: XOrder, first: Int, offset: Int): [X] @dgraph(pred: "~f1") +} + +type Z { + f2(first: Int, offset: Int): [Y] @dgraph(pred: "f2") +} + +####################### +# Extended Definitions +####################### + +scalar DateTime + +enum DgraphIndex { + int + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour +} + +input AuthRule { + and: [AuthRule] + or: [AuthRule] + not: AuthRule + rule: String +} + +enum HTTPMethod { + GET + POST + PUT + PATCH + DELETE +} + +enum Mode { + BATCH + SINGLE +} + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +directive @hasInverse(field: String!) on FIELD_DEFINITION +directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION +directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION +directive @id on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete:AuthRule) on OBJECT +directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE +directive @cascade on FIELD + +input IntFilter { + eq: Int + le: Int + lt: Int + ge: Int + gt: Int +} + +input FloatFilter { + eq: Float + le: Float + lt: Float + ge: Float + gt: Float +} + +input DateTimeFilter { + eq: DateTime + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime +} + +input StringTermFilter { + allofterms: String + anyofterms: String +} + +input StringRegExpFilter { + regexp: String +} + +input StringFullTextFilter { + alloftext: String + anyoftext: String +} + +input StringExactFilter { + eq: String + le: String + lt: String + ge: String + gt: String +} + +input StringHashFilter { + eq: String +} + +####################### +# Generated Types +####################### + +type AddXPayload { + x(filter: XFilter, order: XOrder, first: Int, offset: Int): [X] + numUids: Int +} + +type DeleteXPayload { + x(filter: XFilter, order: XOrder, first: Int, offset: Int): [X] + msg: String + numUids: Int +} + +type UpdateXPayload { + x(filter: XFilter, order: XOrder, first: Int, offset: Int): [X] + numUids: Int +} + +####################### +# Generated Enums +####################### + +enum XOrderable { + name +} + +####################### +# Generated Inputs +####################### + +input AddXInput { + name: String +} + +input UpdateXInput { + filter: XFilter! + set: XPatch + remove: XPatch +} + +input XFilter { + id: [ID!] + not: XFilter +} + +input XOrder { + asc: XOrderable + desc: XOrderable + then: XOrder +} + +input XPatch { + name: String +} + +input XRef { + id: ID + name: String +} + +####################### +# Generated Query +####################### + +type Query { + getX(id: ID!): X + queryX(filter: XFilter, order: XOrder, first: Int, offset: Int): [X] + queryY(first: Int, offset: Int): [Y] + queryZ(first: Int, offset: Int): [Z] +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addX(input: [AddXInput!]!): AddXPayload + updateX(input: UpdateXInput!): UpdateXPayload + deleteX(filter: XFilter!): DeleteXPayload +} + diff --git a/graphql/schema/testdata/schemagen/output/searchables.graphql b/graphql/schema/testdata/schemagen/output/searchables.graphql index f2c54ad1f9f..617ab291a22 100755 --- a/graphql/schema/testdata/schemagen/output/searchables.graphql +++ b/graphql/schema/testdata/schemagen/output/searchables.graphql @@ -180,9 +180,6 @@ enum PostOrderable { title titleByEverything text - tags - tagsHash - tagsExact publishByYear publishByMonth publishByDay diff --git a/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql b/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql new file mode 100644 index 00000000000..cfe162683ff --- /dev/null +++ b/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql @@ -0,0 +1,203 @@ +####################### +# Input Schema +####################### + +type Data { + id: ID! + intList: [Int] + stringList: [String] + metaData(filter: DataFilter): Data +} + +####################### +# Extended Definitions +####################### + +scalar DateTime + +enum DgraphIndex { + int + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour +} + +input AuthRule { + and: [AuthRule] + or: [AuthRule] + not: AuthRule + rule: String +} + +enum HTTPMethod { + GET + POST + PUT + PATCH + DELETE +} + +enum Mode { + BATCH + SINGLE +} + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +directive @hasInverse(field: String!) on FIELD_DEFINITION +directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION +directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION +directive @id on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete:AuthRule) on OBJECT +directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE +directive @cascade on FIELD + +input IntFilter { + eq: Int + le: Int + lt: Int + ge: Int + gt: Int +} + +input FloatFilter { + eq: Float + le: Float + lt: Float + ge: Float + gt: Float +} + +input DateTimeFilter { + eq: DateTime + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime +} + +input StringTermFilter { + allofterms: String + anyofterms: String +} + +input StringRegExpFilter { + regexp: String +} + +input StringFullTextFilter { + alloftext: String + anyoftext: String +} + +input StringExactFilter { + eq: String + le: String + lt: String + ge: String + gt: String +} + +input StringHashFilter { + eq: String +} + +####################### +# Generated Types +####################### + +type AddDataPayload { + data(filter: DataFilter, first: Int, offset: Int): [Data] + numUids: Int +} + +type DeleteDataPayload { + data(filter: DataFilter, first: Int, offset: Int): [Data] + msg: String + numUids: Int +} + +type UpdateDataPayload { + data(filter: DataFilter, first: Int, offset: Int): [Data] + numUids: Int +} + +####################### +# Generated Inputs +####################### + +input AddDataInput { + intList: [Int] + stringList: [String] + metaData: DataRef +} + +input DataFilter { + id: [ID!] + not: DataFilter +} + +input DataPatch { + intList: [Int] + stringList: [String] + metaData: DataRef +} + +input DataRef { + id: ID + intList: [Int] + stringList: [String] + metaData: DataRef +} + +input UpdateDataInput { + filter: DataFilter! + set: DataPatch + remove: DataPatch +} + +####################### +# Generated Query +####################### + +type Query { + getData(id: ID!): Data + queryData(filter: DataFilter, first: Int, offset: Int): [Data] +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addData(input: [AddDataInput!]!): AddDataPayload + updateData(input: UpdateDataInput!): UpdateDataPayload + deleteData(filter: DataFilter!): DeleteDataPayload +} + diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index 32d42087d37..70e5bda1f42 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -519,12 +519,12 @@ func mutatedTypeMapping(s *schema, default: } // This is a convoluted way of getting the type for mutatedTypeName. We get the definition - // for UpdateTPayload and get the type from the first field. There is no direct way to get - // the type from the definition of an object. We use Update and not Add here because - // Interfaces only have Update. + // for AddTPayload and get the type from the first field. There is no direct way to get + // the type from the definition of an object. Interfaces can't have Add and if there is no non Id + // field then Update also will not be there, so we use Delete if there is no AddTPayload. var def *ast.Definition - if def = s.schema.Types["Update"+mutatedTypeName+"Payload"]; def == nil { - def = s.schema.Types["Add"+mutatedTypeName+"Payload"] + if def = s.schema.Types["Add"+mutatedTypeName+"Payload"]; def == nil { + def = s.schema.Types["Delete"+mutatedTypeName+"Payload"] } if def == nil { @@ -675,7 +675,19 @@ func (f *field) DgraphAlias() string { // if this field is repeated, then it should be aliased using its dgraph predicate which will be // unique across repeated fields if f.op.inSchema.repeatedFieldNames[f.Name()] { - return f.DgraphPredicate() + dgraphPredicate := f.DgraphPredicate() + // There won't be any dgraph predicate for fields in introspection queries, as they are not + // stored in dgraph. So we identify those fields using this condition, and just let the + // field name get returned for introspection query fields, because the raw data response is + // prepared for them using only the field name, so that is what should be used to pick them + // back up from that raw data response before completion is performed. + // Now, the reason to not combine this if check with the outer one is because this + // function is performance critical. If there are a million fields in the output, + // it would be called a million times. So, better to perform this check and allocate memory + // for the variable only when necessary to do so. + if dgraphPredicate != "" { + return dgraphPredicate + } } // if not repeated, alias it using its name return f.Name() diff --git a/posting/list.go b/posting/list.go index 5c841971afa..236b11d94d0 100644 --- a/posting/list.go +++ b/posting/list.go @@ -359,8 +359,17 @@ func (l *List) updateMutationLayer(mpost *pb.Posting, singleUidUpdate bool) erro // The current value should be deleted in favor of this value. This needs to // be done because the fingerprint for the value is not math.MaxUint64 as is // the case with the rest of the scalar predicates. - plist := &pb.PostingList{} - plist.Postings = append(plist.Postings, mpost) + newPlist := &pb.PostingList{} + newPlist.Postings = append(newPlist.Postings, mpost) + + // Add the deletions in the existing plist because those postings are not picked + // up by iterating. Not doing so would result in delete operations that are not + // applied when the transaction is committed. + for _, post := range plist.Postings { + if post.Op == Del && post.Uid != mpost.Uid { + newPlist.Postings = append(newPlist.Postings, post) + } + } err := l.iterate(mpost.StartTs, 0, func(obj *pb.Posting) error { // Ignore values which have the same uid as they will get replaced @@ -374,7 +383,7 @@ func (l *List) updateMutationLayer(mpost *pb.Posting, singleUidUpdate bool) erro // for the mutation stored in mpost. objCopy := proto.Clone(obj).(*pb.Posting) objCopy.Op = Del - plist.Postings = append(plist.Postings, objCopy) + newPlist.Postings = append(newPlist.Postings, objCopy) return nil }) if err != nil { @@ -383,7 +392,7 @@ func (l *List) updateMutationLayer(mpost *pb.Posting, singleUidUpdate bool) erro // Update the mutation map with the new plist. Return here since the code below // does not apply for predicates of type uid. - l.mutationMap[mpost.StartTs] = plist + l.mutationMap[mpost.StartTs] = newPlist return nil } @@ -526,9 +535,12 @@ func (l *List) addMutationInternal(ctx context.Context, txn *Txn, t *pb.Directed // getMutation returns a marshaled version of posting list mutation stored internally. func (l *List) getMutation(startTs uint64) []byte { - l.RLock() - defer l.RUnlock() + l.Lock() + defer l.Unlock() if pl, ok := l.mutationMap[startTs]; ok { + for _, p := range pl.GetPostings() { + p.StartTs = 0 + } data, err := pl.Marshal() x.Check(err) return data @@ -596,6 +608,7 @@ func (l *List) pickPostings(readTs uint64) (uint64, []*pb.Posting) { deleteBelowTs = effectiveTs continue } + mpost.StartTs = startTs posts = append(posts, mpost) } } @@ -899,25 +912,7 @@ type rollupOutput struct { newMinTs uint64 } -// Merge all entries in mutation layer with commitTs <= l.commitTs into -// immutable layer. Note that readTs can be math.MaxUint64, so do NOT use it -// directly. It should only serve as the read timestamp for iteration. -func (l *List) rollup(readTs uint64, split bool) (*rollupOutput, error) { - l.AssertRLock() - - // Pick all committed entries - if l.minTs > readTs { - // If we are already past the readTs, then skip the rollup. - return nil, nil - } - - out := &rollupOutput{ - plist: &pb.PostingList{ - Splits: l.plist.Splits, - }, - parts: make(map[uint64]*pb.PostingList), - } - +func (l *List) encode(out *rollupOutput, readTs uint64, split bool) error { var plist *pb.PostingList var startUid, endUid uint64 var splitIdx int @@ -964,19 +959,48 @@ func (l *List) rollup(readTs uint64, split bool) (*rollupOutput, error) { }) // Finish writing the last part of the list (or the whole list if not a multi-part list). if err != nil { - return nil, errors.Wrapf(err, "cannot iterate through the list") + return errors.Wrapf(err, "cannot iterate through the list") } plist.Pack = enc.Done() if plist.Pack != nil { if plist.Pack.BlockSize != uint32(blockSize) { - return nil, errors.Errorf("actual block size %d is different from expected value %d", + return errors.Errorf("actual block size %d is different from expected value %d", plist.Pack.BlockSize, blockSize) } } - - if len(l.plist.Splits) > 0 { + if split && len(l.plist.Splits) > 0 { out.parts[startUid] = plist } + return nil +} + +// Merge all entries in mutation layer with commitTs <= l.commitTs into +// immutable layer. Note that readTs can be math.MaxUint64, so do NOT use it +// directly. It should only serve as the read timestamp for iteration. +func (l *List) rollup(readTs uint64, split bool) (*rollupOutput, error) { + l.AssertRLock() + + // Pick all committed entries + if l.minTs > readTs { + // If we are already past the readTs, then skip the rollup. + return nil, nil + } + + out := &rollupOutput{ + plist: &pb.PostingList{ + Splits: l.plist.Splits, + }, + parts: make(map[uint64]*pb.PostingList), + } + + if len(out.plist.Splits) > 0 || len(l.mutationMap) > 0 { + if err := l.encode(out, readTs, split); err != nil { + return nil, errors.Wrapf(err, "while encoding") + } + } else { + // We already have a nicely packed posting list. Just use it. + out.plist = l.plist + } maxCommitTs := l.minTs { @@ -1350,7 +1374,7 @@ func shouldSplit(plist *pb.PostingList) bool { } func (out *rollupOutput) updateSplits() { - if out.plist == nil { + if out.plist == nil || len(out.parts) > 0 { out.plist = &pb.PostingList{} } out.plist.Splits = out.splits() @@ -1434,13 +1458,11 @@ func binSplit(lowUid uint64, plist *pb.PostingList) ([]uint64, []*pb.PostingList } // Add elements in plist.Postings to the corresponding list. - for _, posting := range plist.Postings { - if posting.Uid < midUid { - lowPl.Postings = append(lowPl.Postings, posting) - } else { - highPl.Postings = append(highPl.Postings, posting) - } - } + pidx := sort.Search(len(plist.Postings), func(idx int) bool { + return plist.Postings[idx].Uid >= midUid + }) + lowPl.Postings = plist.Postings[:pidx] + highPl.Postings = plist.Postings[pidx:] return []uint64{lowUid, midUid}, []*pb.PostingList{lowPl, highPl} } diff --git a/posting/list_test.go b/posting/list_test.go index 9f1ca3c46e8..295e06b9bf8 100644 --- a/posting/list_test.go +++ b/posting/list_test.go @@ -945,6 +945,8 @@ func createMultiPartList(t *testing.T, size int, addLabel bool) (*List, int) { require.NoError(t, writePostingListToDisk(kvs)) ol, err = getNew(key, ps, math.MaxUint64) require.NoError(t, err) + require.Nil(t, ol.plist.Pack) + require.Equal(t, 0, len(ol.plist.Postings)) require.True(t, len(ol.plist.Splits) > 0) verifySplits(t, ol.plist.Splits) diff --git a/posting/lists.go b/posting/lists.go index adf0e3a2ea2..212dcc06f97 100644 --- a/posting/lists.go +++ b/posting/lists.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "io/ioutil" - "math" "os" "os/exec" "runtime" @@ -32,12 +31,12 @@ import ( ostats "go.opencensus.io/stats" "github.com/dgraph-io/badger/v2" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgo/v200/protos/api" - "github.com/golang/glog" - "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" + + "github.com/golang/glog" ) const ( @@ -90,7 +89,7 @@ func getMemUsage() int { return rss * os.Getpagesize() } -func updateMemoryMetrics(lc *y.Closer) { +func updateMemoryMetrics(lc *z.Closer) { defer lc.Done() ticker := time.NewTicker(time.Minute) defer ticker.Stop() @@ -130,13 +129,13 @@ func updateMemoryMetrics(lc *y.Closer) { var ( pstore *badger.DB - closer *y.Closer + closer *z.Closer ) // Init initializes the posting lists package, the in memory and dirty list hash. func Init(ps *badger.DB) { pstore = ps - closer = y.NewCloser(1) + closer = z.NewCloser(1) go updateMemoryMetrics(closer) } @@ -181,6 +180,12 @@ func NewLocalCache(startTs uint64) *LocalCache { } } +// NoCache returns a new LocalCache instance, which won't cache anything. Useful to pass startTs +// around. +func NoCache(startTs uint64) *LocalCache { + return &LocalCache{startTs: startTs} +} + func (lc *LocalCache) getNoStore(key string) *List { lc.RLock() defer lc.RUnlock() @@ -205,8 +210,8 @@ func (lc *LocalCache) SetIfAbsent(key string, updated *List) *List { } func (lc *LocalCache) getInternal(key []byte, readFromDisk bool) (*List, error) { - if lc == nil { - return getNew(key, pstore, math.MaxUint64) + if lc.plists == nil { + return getNew(key, pstore, lc.startTs) } skey := string(key) if pl := lc.getNoStore(skey); pl != nil { diff --git a/posting/mvcc.go b/posting/mvcc.go index cc575e5d31c..7baa91f76c5 100644 --- a/posting/mvcc.go +++ b/posting/mvcc.go @@ -27,7 +27,6 @@ import ( "github.com/dgraph-io/badger/v2" bpb "github.com/dgraph-io/badger/v2/pb" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgo/v200/protos/api" "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/x" @@ -101,7 +100,7 @@ func (ir *incrRollupi) addKeyToBatch(key []byte) { } // Process will rollup batches of 64 keys in a go routine. -func (ir *incrRollupi) Process(closer *y.Closer) { +func (ir *incrRollupi) Process(closer *z.Closer) { defer closer.Done() writer := NewTxnWriter(pstore) @@ -366,6 +365,9 @@ func ReadPostingList(key []byte, it *badger.Iterator) (*List, error) { } func getNew(key []byte, pstore *badger.DB, readTs uint64) (*List, error) { + if pstore.IsClosed() { + return nil, badger.ErrDBClosed + } txn := pstore.NewTransactionAt(readTs, false) defer txn.Discard() diff --git a/protos/pb.proto b/protos/pb.proto index 942083d3726..5b3a05e9ce6 100644 --- a/protos/pb.proto +++ b/protos/pb.proto @@ -219,6 +219,7 @@ message DirectedEdge { } Op op = 8; repeated api.Facet facets = 9; + repeated string allowedPreds = 10; } message Mutations { diff --git a/protos/pb/pb.pb.go b/protos/pb/pb.pb.go index 24683a8ab50..506fd8b77e1 100644 --- a/protos/pb/pb.pb.go +++ b/protos/pb/pb.pb.go @@ -1800,6 +1800,7 @@ type DirectedEdge struct { Lang string `protobuf:"bytes,7,opt,name=lang,proto3" json:"lang,omitempty"` Op DirectedEdge_Op `protobuf:"varint,8,opt,name=op,proto3,enum=pb.DirectedEdge_Op" json:"op,omitempty"` Facets []*api.Facet `protobuf:"bytes,9,rep,name=facets,proto3" json:"facets,omitempty"` + AllowedPreds []string `protobuf:"bytes,10,rep,name=allowedPreds,proto3" json:"allowedPreds,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -1901,6 +1902,13 @@ func (m *DirectedEdge) GetFacets() []*api.Facet { return nil } +func (m *DirectedEdge) GetAllowedPreds() []string { + if m != nil { + return m.AllowedPreds + } + return nil +} + type Mutations struct { GroupId uint32 `protobuf:"varint,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` StartTs uint64 `protobuf:"varint,2,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` @@ -5021,298 +5029,299 @@ func init() { func init() { proto.RegisterFile("pb.proto", fileDescriptor_f80abaa17e25ccc8) } var fileDescriptor_f80abaa17e25ccc8 = []byte{ - // 4652 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x7a, 0xcb, 0x6f, 0x1c, 0x47, - 0x7a, 0xb8, 0xa6, 0xe7, 0xd9, 0xdf, 0x3c, 0x34, 0x2a, 0x69, 0xb5, 0xe3, 0x91, 0x2d, 0xd2, 0x6d, - 0xcb, 0xa6, 0x2d, 0x8b, 0x92, 0xe9, 0xfd, 0xe1, 0xb7, 0xf6, 0x22, 0x40, 0x48, 0x71, 0x24, 0xd3, - 0xe2, 0xcb, 0x3d, 0x43, 0x79, 0xd7, 0x87, 0x0c, 0x6a, 0xba, 0x8b, 0x64, 0x2f, 0x7b, 0xba, 0x7b, - 0xbb, 0x7b, 0x98, 0xa1, 0x4f, 0x09, 0x82, 0xe4, 0x94, 0x5c, 0x12, 0x04, 0xd9, 0x53, 0x92, 0x73, - 0x2e, 0x01, 0x72, 0x0a, 0x72, 0x0e, 0x82, 0x20, 0xa7, 0xfc, 0x05, 0x4a, 0xe0, 0xe4, 0x24, 0x20, - 0x87, 0x20, 0x40, 0x8e, 0x41, 0xf0, 0x7d, 0x55, 0xfd, 0x1a, 0x0e, 0x25, 0x7b, 0x81, 0x3d, 0xe4, - 0x34, 0xf5, 0x3d, 0xea, 0xd1, 0x5f, 0x7d, 0xef, 0x1a, 0x68, 0x04, 0x93, 0xf5, 0x20, 0xf4, 0x63, - 0x9f, 0x69, 0xc1, 0xa4, 0xaf, 0xf3, 0xc0, 0x91, 0x60, 0xff, 0xc3, 0x13, 0x27, 0x3e, 0x9d, 0x4d, - 0xd6, 0x2d, 0x7f, 0xfa, 0xd0, 0x3e, 0x09, 0x79, 0x70, 0xfa, 0xc0, 0xf1, 0x1f, 0x4e, 0xb8, 0x7d, - 0x22, 0xc2, 0x87, 0xe7, 0x1b, 0x0f, 0x83, 0xc9, 0xc3, 0x64, 0x6a, 0xff, 0x41, 0x8e, 0xf7, 0xc4, - 0x3f, 0xf1, 0x1f, 0x12, 0x7a, 0x32, 0x3b, 0x26, 0x88, 0x00, 0x1a, 0x49, 0x76, 0xa3, 0x0f, 0x95, - 0x5d, 0x27, 0x8a, 0x19, 0x83, 0xca, 0xcc, 0xb1, 0xa3, 0x5e, 0x69, 0xb5, 0xbc, 0x56, 0x33, 0x69, - 0x6c, 0xec, 0x81, 0x3e, 0xe2, 0xd1, 0xd9, 0x73, 0xee, 0xce, 0x04, 0xeb, 0x42, 0xf9, 0x9c, 0xbb, - 0xbd, 0xd2, 0x6a, 0x69, 0xad, 0x65, 0xe2, 0x90, 0xad, 0x43, 0xe3, 0x9c, 0xbb, 0xe3, 0xf8, 0x22, - 0x10, 0x3d, 0x6d, 0xb5, 0xb4, 0xd6, 0xd9, 0xb8, 0xb9, 0x1e, 0x4c, 0xd6, 0x0f, 0xfd, 0x28, 0x76, - 0xbc, 0x93, 0xf5, 0xe7, 0xdc, 0x1d, 0x5d, 0x04, 0xc2, 0xac, 0x9f, 0xcb, 0x81, 0x71, 0x00, 0xcd, - 0x61, 0x68, 0x3d, 0x99, 0x79, 0x56, 0xec, 0xf8, 0x1e, 0xee, 0xe8, 0xf1, 0xa9, 0xa0, 0x15, 0x75, - 0x93, 0xc6, 0x88, 0xe3, 0xe1, 0x49, 0xd4, 0x2b, 0xaf, 0x96, 0x11, 0x87, 0x63, 0xd6, 0x83, 0xba, - 0x13, 0x3d, 0xf6, 0x67, 0x5e, 0xdc, 0xab, 0xac, 0x96, 0xd6, 0x1a, 0x66, 0x02, 0x1a, 0x7f, 0x51, - 0x86, 0xea, 0x97, 0x33, 0x11, 0x5e, 0xd0, 0xbc, 0x38, 0x0e, 0x93, 0xb5, 0x70, 0xcc, 0x6e, 0x41, - 0xd5, 0xe5, 0xde, 0x49, 0xd4, 0xd3, 0x68, 0x31, 0x09, 0xb0, 0x3b, 0xa0, 0xf3, 0xe3, 0x58, 0x84, - 0xe3, 0x99, 0x63, 0xf7, 0xca, 0xab, 0xa5, 0xb5, 0x9a, 0xd9, 0x20, 0xc4, 0x91, 0x63, 0xb3, 0x37, - 0xa0, 0x61, 0xfb, 0x63, 0x2b, 0xbf, 0x97, 0xed, 0xd3, 0x5e, 0xec, 0x1d, 0x68, 0xcc, 0x1c, 0x7b, - 0xec, 0x3a, 0x51, 0xdc, 0xab, 0xae, 0x96, 0xd6, 0x9a, 0x1b, 0x0d, 0xfc, 0x58, 0x94, 0x9d, 0x59, - 0x9f, 0x39, 0x36, 0x09, 0xf1, 0x43, 0x68, 0x44, 0xa1, 0x35, 0x3e, 0x9e, 0x79, 0x56, 0xaf, 0x46, - 0x4c, 0xd7, 0x91, 0x29, 0xf7, 0xd5, 0x66, 0x3d, 0x92, 0x00, 0x7e, 0x56, 0x28, 0xce, 0x45, 0x18, - 0x89, 0x5e, 0x5d, 0x6e, 0xa5, 0x40, 0xf6, 0x08, 0x9a, 0xc7, 0xdc, 0x12, 0xf1, 0x38, 0xe0, 0x21, - 0x9f, 0xf6, 0x1a, 0xd9, 0x42, 0x4f, 0x10, 0x7d, 0x88, 0xd8, 0xc8, 0x84, 0xe3, 0x14, 0x60, 0x9f, - 0x40, 0x9b, 0xa0, 0x68, 0x7c, 0xec, 0xb8, 0xb1, 0x08, 0x7b, 0x3a, 0xcd, 0xe9, 0xd0, 0x1c, 0xc2, - 0x8c, 0x42, 0x21, 0xcc, 0x96, 0x64, 0x92, 0x18, 0xf6, 0x16, 0x80, 0x98, 0x07, 0xdc, 0xb3, 0xc7, - 0xdc, 0x75, 0x7b, 0x40, 0x67, 0xd0, 0x25, 0x66, 0xd3, 0x75, 0xd9, 0x0f, 0xf1, 0x7c, 0xdc, 0x1e, - 0xc7, 0x51, 0xaf, 0xbd, 0x5a, 0x5a, 0xab, 0x98, 0x35, 0x04, 0x47, 0x11, 0xca, 0xd5, 0xe2, 0xd6, - 0xa9, 0xe8, 0x75, 0x56, 0x4b, 0x6b, 0x55, 0x53, 0x02, 0x88, 0x3d, 0x76, 0xc2, 0x28, 0xee, 0x5d, - 0x97, 0x58, 0x02, 0x8c, 0x0d, 0xd0, 0x49, 0x7b, 0x48, 0x3a, 0xf7, 0xa0, 0x76, 0x8e, 0x80, 0x54, - 0xb2, 0xe6, 0x46, 0x1b, 0x8f, 0x97, 0x2a, 0x98, 0xa9, 0x88, 0xc6, 0x5d, 0x68, 0xec, 0x72, 0xef, - 0x24, 0xd1, 0x4a, 0xbc, 0x36, 0x9a, 0xa0, 0x9b, 0x34, 0x36, 0x7e, 0xa9, 0x41, 0xcd, 0x14, 0xd1, - 0xcc, 0x8d, 0xd9, 0xfb, 0x00, 0x78, 0x29, 0x53, 0x1e, 0x87, 0xce, 0x5c, 0xad, 0x9a, 0x5d, 0x8b, - 0x3e, 0x73, 0xec, 0x3d, 0x22, 0xb1, 0x47, 0xd0, 0xa2, 0xd5, 0x13, 0x56, 0x2d, 0x3b, 0x40, 0x7a, - 0x3e, 0xb3, 0x49, 0x2c, 0x6a, 0xc6, 0x6d, 0xa8, 0x91, 0x1e, 0x48, 0x5d, 0x6c, 0x9b, 0x0a, 0x62, - 0xf7, 0xa0, 0xe3, 0x78, 0x31, 0xde, 0x93, 0x15, 0x8f, 0x6d, 0x11, 0x25, 0x8a, 0xd2, 0x4e, 0xb1, - 0xdb, 0x22, 0x8a, 0xd9, 0xc7, 0x20, 0x85, 0x9d, 0x6c, 0x58, 0xa5, 0x0d, 0x3b, 0xe9, 0x25, 0x46, - 0x72, 0x47, 0xe2, 0x51, 0x3b, 0x3e, 0x80, 0x26, 0x7e, 0x5f, 0x32, 0xa3, 0x46, 0x33, 0x5a, 0xf4, - 0x35, 0x4a, 0x1c, 0x26, 0x20, 0x83, 0x62, 0x47, 0xd1, 0xa0, 0x32, 0x4a, 0xe5, 0xa1, 0xb1, 0x31, - 0x80, 0xea, 0x41, 0x68, 0x8b, 0x70, 0xa9, 0x3d, 0x30, 0xa8, 0xd8, 0x22, 0xb2, 0xc8, 0x54, 0x1b, - 0x26, 0x8d, 0x33, 0x1b, 0x29, 0xe7, 0x6c, 0xc4, 0xf8, 0xf3, 0x12, 0x34, 0x87, 0x7e, 0x18, 0xef, - 0x89, 0x28, 0xe2, 0x27, 0x82, 0xad, 0x40, 0xd5, 0xc7, 0x65, 0x95, 0x84, 0x75, 0x3c, 0x13, 0xed, - 0x63, 0x4a, 0xfc, 0xc2, 0x3d, 0x68, 0x57, 0xdf, 0x03, 0xea, 0x0e, 0x59, 0x57, 0x59, 0xe9, 0x0e, - 0xd9, 0xd6, 0x6d, 0xa8, 0xf9, 0xc7, 0xc7, 0x91, 0x90, 0xb2, 0xac, 0x9a, 0x0a, 0xba, 0x52, 0x05, - 0x8d, 0xff, 0x07, 0x80, 0xe7, 0xfb, 0x9e, 0x5a, 0x60, 0x9c, 0x42, 0xd3, 0xe4, 0xc7, 0xf1, 0x63, - 0xdf, 0x8b, 0xc5, 0x3c, 0x66, 0x1d, 0xd0, 0x1c, 0x9b, 0x44, 0x54, 0x33, 0x35, 0xc7, 0xc6, 0xc3, - 0x9d, 0x84, 0xfe, 0x2c, 0x20, 0x09, 0xb5, 0x4d, 0x09, 0x90, 0x28, 0x6d, 0x3b, 0xa4, 0x13, 0xa3, - 0x28, 0x6d, 0x3b, 0x64, 0x2b, 0xd0, 0x8c, 0x3c, 0x1e, 0x44, 0xa7, 0x7e, 0x8c, 0x87, 0xab, 0xd0, - 0xe1, 0x20, 0x41, 0x8d, 0x22, 0xe3, 0x3f, 0x34, 0xa8, 0xed, 0x89, 0xe9, 0x44, 0x84, 0x97, 0x76, - 0x79, 0x04, 0x0d, 0x5a, 0x78, 0xec, 0xd8, 0x72, 0xa3, 0xad, 0x1f, 0xbc, 0x7c, 0xb1, 0x72, 0x83, - 0x70, 0x3b, 0xf6, 0x47, 0xfe, 0xd4, 0x89, 0xc5, 0x34, 0x88, 0x2f, 0xcc, 0xba, 0x42, 0x2d, 0x3d, - 0xc1, 0x6d, 0xa8, 0xb9, 0x82, 0xe3, 0x9d, 0x48, 0xf5, 0x53, 0x10, 0x7b, 0x00, 0x75, 0x3e, 0x1d, - 0xdb, 0x82, 0xdb, 0xe4, 0xa5, 0x1a, 0x5b, 0xb7, 0x5e, 0xbe, 0x58, 0xe9, 0xf2, 0xe9, 0xb6, 0xe0, - 0xf9, 0xb5, 0x6b, 0x12, 0xc3, 0x3e, 0x45, 0x9d, 0x8b, 0xe2, 0xf1, 0x2c, 0xb0, 0x79, 0x2c, 0xc8, - 0x67, 0x55, 0xb6, 0x7a, 0x2f, 0x5f, 0xac, 0xdc, 0x42, 0xf4, 0x11, 0x61, 0x73, 0xd3, 0x20, 0xc3, - 0xb2, 0x1d, 0xb8, 0x61, 0xb9, 0xb3, 0x08, 0x5d, 0xa9, 0xe3, 0x1d, 0xfb, 0x63, 0xdf, 0x73, 0x2f, - 0xe8, 0x9a, 0x1a, 0x5b, 0x6f, 0xbd, 0x7c, 0xb1, 0xf2, 0x86, 0x22, 0xee, 0x78, 0xc7, 0xfe, 0x81, - 0xe7, 0x5e, 0xe4, 0x56, 0xb9, 0xbe, 0x40, 0x62, 0xbf, 0x09, 0x9d, 0x63, 0x3f, 0xb4, 0xc4, 0x38, - 0x15, 0x4c, 0x87, 0xd6, 0xe9, 0xbf, 0x7c, 0xb1, 0x72, 0x9b, 0x28, 0x4f, 0x2f, 0x49, 0xa7, 0x95, - 0xc7, 0x1b, 0x7f, 0xab, 0x41, 0x95, 0xc6, 0xec, 0x11, 0xd4, 0xa7, 0x24, 0xf8, 0xc4, 0xcb, 0xdc, - 0x46, 0x4d, 0x20, 0xda, 0xba, 0xbc, 0x91, 0x68, 0xe0, 0xc5, 0xe1, 0x85, 0x99, 0xb0, 0xe1, 0x8c, - 0x98, 0x4f, 0x5c, 0x11, 0x47, 0x4a, 0x73, 0x73, 0x33, 0x46, 0x92, 0xa0, 0x66, 0x28, 0xb6, 0xc5, - 0xeb, 0x2f, 0x2f, 0x5e, 0x3f, 0xeb, 0x43, 0xc3, 0x3a, 0x15, 0xd6, 0x59, 0x34, 0x9b, 0x2a, 0xe5, - 0x48, 0xe1, 0xfe, 0x13, 0x68, 0xe5, 0xcf, 0x81, 0x71, 0xf5, 0x4c, 0x5c, 0x90, 0x82, 0x54, 0x4c, - 0x1c, 0xb2, 0x55, 0xa8, 0x92, 0x27, 0x22, 0xf5, 0x68, 0x6e, 0x00, 0x1e, 0x47, 0x4e, 0x31, 0x25, - 0xe1, 0x33, 0xed, 0xc7, 0x25, 0x5c, 0x27, 0x7f, 0xba, 0xfc, 0x3a, 0xfa, 0xd5, 0xeb, 0xc8, 0x29, - 0xb9, 0x75, 0x0c, 0x1f, 0xea, 0xbb, 0x8e, 0x25, 0xbc, 0x88, 0xa2, 0xef, 0x2c, 0x12, 0xa9, 0xd7, - 0xc0, 0x31, 0x7e, 0xca, 0x94, 0xcf, 0xf7, 0x7d, 0x5b, 0x44, 0xb4, 0x4e, 0xc5, 0x4c, 0x61, 0xa4, - 0x89, 0x79, 0xe0, 0x84, 0x17, 0x23, 0x29, 0x84, 0xb2, 0x99, 0xc2, 0x18, 0xde, 0x84, 0x87, 0x9b, - 0xd9, 0x49, 0x24, 0x55, 0xa0, 0xf1, 0x97, 0x65, 0x68, 0x7d, 0x2d, 0x42, 0xff, 0x30, 0xf4, 0x03, - 0x3f, 0xe2, 0x2e, 0xdb, 0x2c, 0x8a, 0x53, 0x5e, 0xdb, 0x2a, 0x9e, 0x36, 0xcf, 0xb6, 0x3e, 0x4c, - 0xe5, 0x2b, 0xaf, 0x23, 0x2f, 0x70, 0x03, 0x6a, 0xf2, 0x3a, 0x97, 0xc8, 0x4c, 0x51, 0x90, 0x47, - 0x5e, 0x20, 0x9d, 0xb5, 0x28, 0x0f, 0x45, 0x61, 0x77, 0x01, 0xa6, 0x7c, 0xbe, 0x2b, 0x78, 0x24, - 0x76, 0xec, 0xc4, 0xae, 0x33, 0x8c, 0x92, 0xc6, 0x68, 0xee, 0x8d, 0x22, 0xb2, 0x2f, 0x29, 0x0d, - 0x82, 0xd9, 0x9b, 0xa0, 0x4f, 0xf9, 0x1c, 0x1d, 0xcc, 0x8e, 0x2d, 0x2d, 0xc9, 0xcc, 0x10, 0xec, - 0x6d, 0x28, 0xc7, 0x73, 0x8f, 0xbc, 0x35, 0x06, 0x73, 0xcc, 0xed, 0x46, 0x73, 0x4f, 0xb9, 0x22, - 0x13, 0x69, 0xc9, 0x0d, 0x36, 0xb2, 0x1b, 0xec, 0x42, 0xd9, 0x72, 0x6c, 0x8a, 0xe6, 0xba, 0x89, - 0x43, 0x76, 0x0f, 0xea, 0xae, 0xbc, 0x2d, 0x8a, 0xd8, 0xcd, 0x8d, 0xa6, 0x74, 0x74, 0x84, 0x32, - 0x13, 0x5a, 0xff, 0x37, 0xe0, 0xfa, 0x82, 0xb8, 0xf2, 0xfa, 0xd1, 0x96, 0xab, 0xdf, 0xca, 0xeb, - 0x47, 0x25, 0xaf, 0x13, 0xff, 0x52, 0x86, 0xeb, 0x4a, 0x49, 0x4f, 0x9d, 0x60, 0x18, 0xa3, 0xbd, - 0xf7, 0xa0, 0x4e, 0xde, 0x5a, 0xe9, 0x47, 0xc5, 0x4c, 0x40, 0xf6, 0xff, 0xa1, 0x46, 0x86, 0x9b, - 0xd8, 0xcf, 0x4a, 0x26, 0xfc, 0x74, 0xba, 0xb4, 0x27, 0x75, 0x73, 0x8a, 0x9d, 0xfd, 0x08, 0xaa, - 0xdf, 0x88, 0xd0, 0x97, 0xd1, 0xa7, 0xb9, 0x71, 0x77, 0xd9, 0x3c, 0x54, 0x01, 0x35, 0x4d, 0x32, - 0xff, 0x1a, 0xef, 0xe8, 0x5d, 0x8c, 0x37, 0x53, 0xff, 0x5c, 0xd8, 0xbd, 0x3a, 0x9d, 0x28, 0xaf, - 0x46, 0x09, 0x29, 0xb9, 0x94, 0xc6, 0xd2, 0x4b, 0xd1, 0x5f, 0x71, 0x29, 0xdb, 0xd0, 0xcc, 0x49, - 0x61, 0xc9, 0x85, 0xac, 0x14, 0x0d, 0x56, 0x4f, 0xfd, 0x50, 0xde, 0xee, 0xb7, 0x01, 0x32, 0x99, - 0xfc, 0xaa, 0xde, 0xc3, 0xf8, 0xdd, 0x12, 0x5c, 0x7f, 0xec, 0x7b, 0x9e, 0xa0, 0xac, 0x54, 0xde, - 0x70, 0x66, 0x44, 0xa5, 0x2b, 0x8d, 0xe8, 0x03, 0xa8, 0x46, 0xc8, 0xac, 0x56, 0xbf, 0xb9, 0xe4, - 0xca, 0x4c, 0xc9, 0x81, 0x5e, 0x72, 0xca, 0xe7, 0xe3, 0x40, 0x78, 0xb6, 0xe3, 0x9d, 0x24, 0x5e, - 0x72, 0xca, 0xe7, 0x87, 0x12, 0x63, 0xfc, 0xa9, 0x06, 0xf0, 0xb9, 0xe0, 0x6e, 0x7c, 0x8a, 0x91, - 0x00, 0xef, 0xcd, 0xf1, 0xa2, 0x98, 0x7b, 0x56, 0x52, 0x13, 0xa4, 0x30, 0x2a, 0x1f, 0x86, 0x3d, - 0x11, 0x49, 0x27, 0xa4, 0x9b, 0x09, 0x88, 0x81, 0x10, 0xb7, 0x9b, 0x45, 0x2a, 0x3c, 0x2a, 0x28, - 0x0b, 0xe6, 0x15, 0x42, 0xab, 0x60, 0xde, 0x83, 0x3a, 0xe6, 0xd8, 0x8e, 0xef, 0x91, 0x6a, 0xe8, - 0x66, 0x02, 0xe2, 0x3a, 0xb3, 0x20, 0x76, 0xa6, 0x32, 0x08, 0x96, 0x4d, 0x05, 0xe1, 0xa9, 0x30, - 0xe8, 0x0d, 0xac, 0x53, 0x9f, 0x8c, 0xb7, 0x6c, 0xa6, 0x30, 0xae, 0xe6, 0x7b, 0x27, 0x3e, 0x7e, - 0x5d, 0x83, 0xf2, 0xa7, 0x04, 0x94, 0xdf, 0x62, 0x8b, 0x39, 0x92, 0x74, 0x22, 0xa5, 0x30, 0xca, - 0x45, 0x88, 0xf1, 0xb1, 0xe0, 0xf1, 0x2c, 0x14, 0x51, 0x0f, 0x88, 0x0c, 0x42, 0x3c, 0x51, 0x18, - 0xe3, 0x77, 0x34, 0xa8, 0x49, 0xbf, 0x54, 0x48, 0x16, 0x4a, 0xdf, 0x29, 0x59, 0x78, 0x13, 0xf4, - 0x20, 0x14, 0xb6, 0x63, 0x25, 0x97, 0xa4, 0x9b, 0x19, 0x82, 0xb2, 0x74, 0x8c, 0x9b, 0x24, 0xac, - 0x86, 0x29, 0x01, 0xc4, 0x46, 0x01, 0xb7, 0x84, 0xfa, 0x40, 0x09, 0xa0, 0x44, 0xa4, 0xca, 0x93, - 0xaa, 0x37, 0x4c, 0x05, 0xb1, 0x4f, 0x40, 0xa7, 0xac, 0x8c, 0x02, 0xbe, 0x4e, 0x81, 0xfa, 0xf6, - 0xcb, 0x17, 0x2b, 0x0c, 0x91, 0x0b, 0x91, 0xbe, 0x91, 0xe0, 0x30, 0x2f, 0xc1, 0xc9, 0xe8, 0xdf, - 0x81, 0x92, 0x0c, 0xca, 0x4b, 0x10, 0x35, 0x8a, 0xf2, 0x79, 0x89, 0xc4, 0x18, 0x7f, 0xa5, 0x41, - 0x6b, 0xdb, 0x09, 0x85, 0x15, 0x0b, 0x7b, 0x60, 0x9f, 0xd0, 0x61, 0x84, 0x17, 0x3b, 0xf1, 0x85, - 0xca, 0xa4, 0x14, 0x94, 0x26, 0xba, 0x5a, 0xb1, 0xf0, 0x93, 0x16, 0x50, 0xa6, 0x5a, 0x55, 0x02, - 0x6c, 0x03, 0x40, 0x96, 0x00, 0x54, 0xaf, 0x56, 0xae, 0xae, 0x57, 0x75, 0x62, 0xc3, 0x21, 0xd6, - 0x83, 0x72, 0x8e, 0x23, 0xd3, 0xa9, 0x1a, 0x15, 0xb3, 0x33, 0xf4, 0x32, 0x94, 0x39, 0x4f, 0x84, - 0x4b, 0xea, 0x42, 0x99, 0xf3, 0x44, 0xb8, 0x69, 0xbd, 0x52, 0x97, 0xc7, 0xc1, 0x31, 0x7b, 0x07, - 0x34, 0x3f, 0x20, 0x19, 0xaa, 0x0d, 0xf3, 0x1f, 0xb6, 0x7e, 0x10, 0x98, 0x9a, 0x1f, 0xa0, 0xed, - 0xc9, 0xe2, 0x8c, 0xd4, 0x05, 0x6d, 0x0f, 0x23, 0x04, 0x95, 0x0a, 0xa6, 0xa2, 0x18, 0xb7, 0x41, - 0x3b, 0x08, 0x58, 0x1d, 0xca, 0xc3, 0xc1, 0xa8, 0x7b, 0x0d, 0x07, 0xdb, 0x83, 0xdd, 0x6e, 0xc9, - 0xf8, 0x56, 0x03, 0x7d, 0x6f, 0x16, 0x73, 0xb4, 0xe4, 0x08, 0xcf, 0x5c, 0x54, 0x99, 0x4c, 0x37, - 0xde, 0x80, 0x46, 0x14, 0xf3, 0x90, 0xa2, 0xac, 0xf4, 0xf9, 0x75, 0x82, 0x47, 0x11, 0x7b, 0x0f, - 0xaa, 0xc2, 0x3e, 0x11, 0x89, 0x2b, 0xee, 0x2e, 0x9e, 0xd3, 0x94, 0x64, 0xb6, 0x06, 0xb5, 0xc8, - 0x3a, 0x15, 0x53, 0xde, 0xab, 0x64, 0x8c, 0x43, 0xc2, 0xc8, 0xbc, 0xd0, 0x54, 0x74, 0xf6, 0x2e, - 0x54, 0x51, 0xd2, 0x91, 0x2a, 0x64, 0xa8, 0xf4, 0x41, 0xa1, 0x2a, 0x36, 0x49, 0x44, 0xbd, 0xb0, - 0x43, 0x3f, 0x18, 0xfb, 0x01, 0xc9, 0xac, 0xb3, 0x71, 0x8b, 0x3c, 0x4a, 0xf2, 0x35, 0xeb, 0xdb, - 0xa1, 0x1f, 0x1c, 0x04, 0x66, 0xcd, 0xa6, 0x5f, 0xac, 0x59, 0x89, 0x5d, 0xde, 0xaf, 0x74, 0xc1, - 0x3a, 0x62, 0x64, 0x8f, 0x62, 0x0d, 0x1a, 0x53, 0x11, 0x73, 0x9b, 0xc7, 0x5c, 0x79, 0xe2, 0x96, - 0x74, 0x50, 0x12, 0x67, 0xa6, 0x54, 0xe3, 0x21, 0xd4, 0xe4, 0xd2, 0xac, 0x01, 0x95, 0xfd, 0x83, - 0xfd, 0x81, 0x14, 0xe8, 0xe6, 0xee, 0x6e, 0xb7, 0x84, 0xa8, 0xed, 0xcd, 0xd1, 0x66, 0x57, 0xc3, - 0xd1, 0xe8, 0x67, 0x87, 0x83, 0x6e, 0xd9, 0xf8, 0xa7, 0x12, 0x34, 0x92, 0x75, 0xd8, 0x67, 0x00, - 0x68, 0x53, 0xe3, 0x53, 0xc7, 0x4b, 0x13, 0x96, 0x3b, 0xf9, 0x9d, 0xd6, 0x0f, 0x43, 0x61, 0x7f, - 0x8e, 0x54, 0x19, 0xba, 0xc8, 0x04, 0x09, 0xee, 0x0f, 0xa1, 0x53, 0x24, 0x2e, 0xc9, 0xdc, 0xee, - 0xe7, 0x7d, 0x78, 0x67, 0xe3, 0x07, 0x85, 0xa5, 0x71, 0x26, 0x29, 0x6a, 0xce, 0x9d, 0x3f, 0x80, - 0x46, 0x82, 0x66, 0x4d, 0xa8, 0x6f, 0x0f, 0x9e, 0x6c, 0x1e, 0xed, 0xa2, 0x92, 0x00, 0xd4, 0x86, - 0x3b, 0xfb, 0x4f, 0x77, 0x07, 0xf2, 0xb3, 0x76, 0x77, 0x86, 0xa3, 0xae, 0x66, 0xfc, 0x49, 0x09, - 0x1a, 0x49, 0x7e, 0xc0, 0x3e, 0xc0, 0xc0, 0x4e, 0x69, 0x88, 0xf2, 0xfb, 0xd4, 0x6a, 0xc8, 0x15, - 0x4a, 0x66, 0x42, 0x47, 0xa5, 0x27, 0x37, 0x96, 0x64, 0x0c, 0x04, 0xe4, 0xcb, 0xb4, 0x72, 0xa1, - 0x53, 0x80, 0x15, 0xa7, 0xef, 0x09, 0x95, 0x00, 0xd2, 0x98, 0x74, 0xd0, 0xf1, 0x2c, 0xf2, 0x04, - 0x55, 0xa5, 0x83, 0x08, 0x8f, 0x22, 0xe3, 0x8f, 0x2b, 0xd0, 0x31, 0x45, 0x14, 0xfb, 0xa1, 0x30, - 0xc5, 0x2f, 0x66, 0x58, 0x46, 0xbf, 0x42, 0x99, 0xdf, 0x02, 0x08, 0x25, 0x73, 0xa6, 0xce, 0xba, - 0xc2, 0xc8, 0x14, 0xdc, 0xf5, 0x2d, 0xd2, 0x22, 0x15, 0x19, 0x52, 0x98, 0xdd, 0x01, 0x7d, 0xc2, - 0xad, 0x33, 0xb9, 0xac, 0x8c, 0x0f, 0x0d, 0x89, 0x90, 0xeb, 0x72, 0xcb, 0x12, 0x51, 0x34, 0xc6, - 0x4b, 0x91, 0x51, 0x42, 0x97, 0x98, 0x67, 0xe2, 0x02, 0xc9, 0x91, 0xb0, 0x42, 0x11, 0x13, 0x59, - 0x1a, 0xbf, 0x2e, 0x31, 0x48, 0x7e, 0x07, 0xda, 0x91, 0x88, 0x30, 0xa2, 0x8c, 0x63, 0xff, 0x4c, - 0x78, 0xca, 0x13, 0xb4, 0x14, 0x72, 0x84, 0x38, 0xf4, 0xd1, 0xdc, 0xf3, 0xbd, 0x8b, 0xa9, 0x3f, - 0x8b, 0x94, 0x73, 0xcd, 0x10, 0x6c, 0x1d, 0x6e, 0x0a, 0xcf, 0x0a, 0x2f, 0x02, 0x3c, 0x2b, 0xee, - 0x32, 0x3e, 0x76, 0x5c, 0xa1, 0x92, 0xc0, 0x1b, 0x19, 0xe9, 0x99, 0xb8, 0x78, 0xe2, 0xb8, 0x02, - 0x4f, 0x74, 0xce, 0x67, 0x6e, 0x3c, 0xa6, 0x22, 0x11, 0xe4, 0x89, 0x08, 0xb3, 0x89, 0x95, 0xe2, - 0x87, 0x70, 0x43, 0x92, 0x43, 0xdf, 0x15, 0x8e, 0x2d, 0x17, 0x6b, 0x12, 0xd7, 0x75, 0x22, 0x98, - 0x84, 0xa7, 0xa5, 0xd6, 0xe1, 0xa6, 0xe4, 0x95, 0x1f, 0x94, 0x70, 0xb7, 0xe4, 0xd6, 0x44, 0x1a, - 0x2a, 0x4a, 0x71, 0xeb, 0x80, 0xc7, 0xa7, 0x54, 0xfc, 0x25, 0x5b, 0x1f, 0xf2, 0xf8, 0x14, 0x23, - 0x9d, 0x24, 0x1f, 0x3b, 0xc2, 0x95, 0x45, 0x9d, 0x6e, 0xca, 0x19, 0x4f, 0x10, 0xc3, 0xde, 0x86, - 0x96, 0x62, 0xf0, 0xc3, 0x29, 0x97, 0xbd, 0x23, 0xdd, 0x94, 0x93, 0x9e, 0x10, 0xca, 0xf8, 0x1f, - 0x0d, 0x1a, 0x69, 0xa5, 0x70, 0x1f, 0xf4, 0x69, 0xe2, 0x1a, 0x54, 0x06, 0xd2, 0x2e, 0xf8, 0x0b, - 0x33, 0xa3, 0xb3, 0xb7, 0x40, 0x3b, 0x3b, 0x57, 0x6e, 0xaa, 0xbd, 0x2e, 0x9b, 0xa5, 0xc1, 0x64, - 0x63, 0xfd, 0xd9, 0x73, 0x53, 0x3b, 0x3b, 0xcf, 0x32, 0x99, 0xea, 0x6b, 0x33, 0x99, 0xf7, 0xe1, - 0xba, 0xe5, 0x0a, 0xee, 0x8d, 0xb3, 0xc8, 0x2a, 0x2f, 0xbe, 0x43, 0xe8, 0xc3, 0x34, 0xbc, 0x2a, - 0x4b, 0xae, 0x67, 0x96, 0x7c, 0x0f, 0xaa, 0xb6, 0x70, 0x63, 0x9e, 0xef, 0xe2, 0x1d, 0x84, 0xdc, - 0x72, 0xc5, 0x36, 0xa2, 0x4d, 0x49, 0x45, 0xc7, 0x95, 0x54, 0x33, 0x79, 0xc7, 0x95, 0xd8, 0xa8, - 0x99, 0x52, 0x33, 0x13, 0x84, 0xbc, 0x09, 0xde, 0x87, 0x1b, 0x62, 0x1e, 0x90, 0xb7, 0x1e, 0xa7, - 0x95, 0x67, 0x93, 0x38, 0xba, 0x09, 0xe1, 0xb1, 0xc2, 0xb3, 0x8f, 0xd0, 0x5e, 0xc9, 0x4e, 0xe8, - 0x66, 0x9b, 0x1b, 0x8c, 0x0c, 0xbe, 0x60, 0x79, 0x66, 0xc2, 0x62, 0x78, 0x50, 0x7e, 0xf6, 0x7c, - 0xa8, 0xa4, 0x59, 0xba, 0x4a, 0x9a, 0x89, 0xa9, 0x6b, 0x39, 0x53, 0xbf, 0x2b, 0xbd, 0x24, 0x89, - 0x26, 0xe9, 0x30, 0xe5, 0x30, 0xf8, 0x29, 0x32, 0x42, 0x54, 0x64, 0xf3, 0x89, 0x00, 0xe3, 0xbf, - 0xcb, 0x50, 0x57, 0x21, 0x19, 0xe5, 0x39, 0x4b, 0x9b, 0x27, 0x38, 0x2c, 0xd6, 0x2c, 0x69, 0x6c, - 0xcf, 0x77, 0xa2, 0xcb, 0xaf, 0xef, 0x44, 0xb3, 0xcf, 0xa0, 0x15, 0x48, 0x5a, 0x3e, 0x1b, 0xf8, - 0x61, 0x7e, 0x8e, 0xfa, 0xa5, 0x79, 0xcd, 0x20, 0x03, 0xd0, 0x25, 0x51, 0x9b, 0x2e, 0xe6, 0x27, - 0xa4, 0x3a, 0x2d, 0xb3, 0x8e, 0xf0, 0x88, 0x9f, 0x5c, 0x91, 0x13, 0x7c, 0x87, 0xd0, 0xce, 0x3a, - 0x94, 0x23, 0xb4, 0xc8, 0xc3, 0x61, 0x3a, 0x90, 0x8f, 0xd4, 0xed, 0x62, 0xa4, 0xbe, 0x03, 0xba, - 0xe5, 0x4f, 0xa7, 0x0e, 0xd1, 0x3a, 0xaa, 0xb9, 0x40, 0x88, 0x51, 0x64, 0xfc, 0x41, 0x09, 0xea, - 0xea, 0x6b, 0x2f, 0xc5, 0x81, 0xad, 0x9d, 0xfd, 0x4d, 0xf3, 0x67, 0xdd, 0x12, 0xc6, 0xb9, 0x9d, - 0xfd, 0x51, 0x57, 0x63, 0x3a, 0x54, 0x9f, 0xec, 0x1e, 0x6c, 0x8e, 0xba, 0x65, 0x8c, 0x0d, 0x5b, - 0x07, 0x07, 0xbb, 0xdd, 0x0a, 0x6b, 0x41, 0x63, 0x7b, 0x73, 0x34, 0x18, 0xed, 0xec, 0x0d, 0xba, - 0x55, 0xe4, 0x7d, 0x3a, 0x38, 0xe8, 0xd6, 0x70, 0x70, 0xb4, 0xb3, 0xdd, 0xad, 0x23, 0xfd, 0x70, - 0x73, 0x38, 0xfc, 0xea, 0xc0, 0xdc, 0xee, 0x36, 0x28, 0xbe, 0x8c, 0xcc, 0x9d, 0xfd, 0xa7, 0x5d, - 0x1d, 0xc7, 0x07, 0x5b, 0x5f, 0x0c, 0x1e, 0x8f, 0xba, 0x60, 0x7c, 0x0c, 0xcd, 0x9c, 0x04, 0x71, - 0xb6, 0x39, 0x78, 0xd2, 0xbd, 0x86, 0x5b, 0x3e, 0xdf, 0xdc, 0x3d, 0xc2, 0x70, 0xd4, 0x01, 0xa0, - 0xe1, 0x78, 0x77, 0x73, 0xff, 0x69, 0x57, 0x33, 0xbe, 0x84, 0xc6, 0x91, 0x63, 0x6f, 0xb9, 0xbe, - 0x75, 0x86, 0xea, 0x34, 0xe1, 0x91, 0x50, 0x75, 0x0d, 0x8d, 0x31, 0x05, 0x24, 0x63, 0x89, 0xd4, - 0xdd, 0x2b, 0x08, 0x65, 0xe5, 0xcd, 0xa6, 0x63, 0x7a, 0xbd, 0x28, 0xcb, 0x18, 0xe1, 0xcd, 0xa6, - 0x47, 0x8e, 0x1d, 0x19, 0xfb, 0x50, 0x3f, 0x72, 0xec, 0x43, 0x6e, 0x9d, 0xa1, 0xab, 0x9a, 0xe0, - 0xd2, 0xe3, 0xc8, 0xf9, 0x46, 0xa8, 0x58, 0xa2, 0x13, 0x66, 0xe8, 0x7c, 0x23, 0xd8, 0xbb, 0x50, - 0x23, 0x20, 0xa9, 0x61, 0xc9, 0xfc, 0x92, 0xe3, 0x98, 0x8a, 0x66, 0xfc, 0x61, 0x29, 0xfd, 0x2c, - 0x6a, 0x4f, 0xaf, 0x40, 0x25, 0xe0, 0xd6, 0x99, 0x8a, 0x9b, 0x4d, 0x35, 0x07, 0xf7, 0x33, 0x89, - 0xc0, 0xde, 0x87, 0x86, 0xd2, 0x9d, 0x64, 0xe1, 0x66, 0x4e, 0xc9, 0xcc, 0x94, 0x58, 0xbc, 0xd5, - 0x72, 0xf1, 0x56, 0xa9, 0xc6, 0x09, 0x5c, 0x27, 0x96, 0x96, 0x52, 0x31, 0x15, 0x64, 0xfc, 0x08, - 0x20, 0x7b, 0x11, 0x58, 0x92, 0x46, 0xdc, 0x82, 0x2a, 0x77, 0x1d, 0x9e, 0xd4, 0x4c, 0x12, 0x30, - 0xf6, 0xa1, 0x99, 0x7b, 0x47, 0x40, 0xf1, 0x71, 0xd7, 0xc5, 0x38, 0x13, 0xd1, 0xdc, 0x86, 0x59, - 0xe7, 0xae, 0xfb, 0x4c, 0x5c, 0x44, 0x98, 0xc2, 0xc9, 0x27, 0x08, 0x6d, 0xa1, 0x7b, 0x4d, 0x53, - 0x4d, 0x49, 0x34, 0x3e, 0x82, 0x9a, 0x6c, 0x69, 0xe7, 0x34, 0xbd, 0x74, 0x65, 0x12, 0xfb, 0xa9, - 0x3a, 0x33, 0x35, 0xc0, 0xd9, 0x7d, 0xf5, 0xd4, 0x11, 0xc9, 0x87, 0x95, 0x52, 0x56, 0x75, 0x4b, - 0x26, 0xf5, 0xca, 0x41, 0xcc, 0xc6, 0x36, 0x34, 0x5e, 0xf9, 0x78, 0xa4, 0x04, 0xa0, 0x65, 0x02, - 0x58, 0xf2, 0x9c, 0x64, 0xfc, 0x1c, 0x20, 0x7b, 0x12, 0x51, 0x86, 0x27, 0x57, 0x41, 0xc3, 0xfb, - 0x10, 0x1a, 0xd6, 0xa9, 0xe3, 0xda, 0xa1, 0xf0, 0x0a, 0x5f, 0x9d, 0x3d, 0xa2, 0xa4, 0x74, 0xb6, - 0x0a, 0x15, 0x7a, 0xe9, 0x29, 0x67, 0x0e, 0x3b, 0x7d, 0xe6, 0x21, 0x8a, 0x31, 0x87, 0xb6, 0xcc, - 0x8d, 0xbf, 0x43, 0x3e, 0x53, 0xf4, 0x96, 0xda, 0x25, 0x6f, 0x79, 0x1b, 0x6a, 0x14, 0x46, 0x93, - 0xaf, 0x51, 0xd0, 0x15, 0x5e, 0xf4, 0xf7, 0x34, 0x00, 0xb9, 0xf5, 0xbe, 0x6f, 0x8b, 0x62, 0x55, - 0x58, 0x5a, 0xac, 0x0a, 0x19, 0x54, 0xd2, 0x47, 0x3c, 0xdd, 0xa4, 0x71, 0x16, 0x67, 0x54, 0xa5, - 0x28, 0xe3, 0xcc, 0x9b, 0xa0, 0x53, 0x5a, 0xe3, 0x7c, 0x43, 0x9d, 0x67, 0xdc, 0x30, 0x43, 0xe4, - 0x9f, 0xb4, 0xaa, 0xc5, 0x27, 0xad, 0xb4, 0xef, 0x5f, 0x93, 0xab, 0xc9, 0xbe, 0xff, 0x92, 0x27, - 0x0c, 0x59, 0x87, 0x47, 0x22, 0x8c, 0x93, 0xaa, 0x53, 0x42, 0x69, 0x65, 0xa5, 0x2b, 0x5e, 0x2e, - 0x2b, 0x69, 0xcf, 0x1f, 0x5b, 0xbe, 0x77, 0xec, 0x3a, 0x56, 0xac, 0x9e, 0xb0, 0xc0, 0xf3, 0x1f, - 0x2b, 0x8c, 0xf1, 0x19, 0xb4, 0x12, 0xf9, 0xd3, 0x4b, 0xc1, 0x87, 0x69, 0xf5, 0x52, 0xca, 0xee, - 0x36, 0x13, 0xd3, 0x96, 0xd6, 0x2b, 0x25, 0xf5, 0x8b, 0xf1, 0x5f, 0xe5, 0x64, 0xb2, 0x6a, 0x78, - 0xbf, 0x5a, 0x86, 0xc5, 0xf2, 0x52, 0xfb, 0x4e, 0xe5, 0xe5, 0x8f, 0x41, 0xb7, 0xa9, 0xc6, 0x72, - 0xce, 0x93, 0xb8, 0xd5, 0x5f, 0xac, 0xa7, 0x54, 0x15, 0xe6, 0x9c, 0x0b, 0x33, 0x63, 0x7e, 0xcd, - 0x3d, 0xa4, 0xd2, 0xae, 0x2e, 0x93, 0x76, 0xed, 0x57, 0x94, 0xf6, 0xdb, 0xd0, 0xf2, 0x7c, 0x6f, - 0xec, 0xcd, 0x5c, 0x97, 0x4f, 0x5c, 0xa1, 0xc4, 0xdd, 0xf4, 0x7c, 0x6f, 0x5f, 0xa1, 0x30, 0xd7, - 0xcc, 0xb3, 0x48, 0xa3, 0x6e, 0x12, 0xdf, 0xf5, 0x1c, 0x1f, 0x99, 0xfe, 0x1a, 0x74, 0xfd, 0xc9, - 0xcf, 0x85, 0x15, 0x93, 0xc4, 0xc6, 0x64, 0xcd, 0x32, 0xd1, 0xec, 0x48, 0x3c, 0x8a, 0x68, 0x1f, - 0xed, 0x7a, 0xe1, 0x9a, 0xdb, 0x97, 0xae, 0xf9, 0x53, 0xd0, 0x53, 0x29, 0xe5, 0xea, 0x39, 0x1d, - 0xaa, 0x3b, 0xfb, 0xdb, 0x83, 0x9f, 0x76, 0x4b, 0x18, 0x0b, 0xcd, 0xc1, 0xf3, 0x81, 0x39, 0x1c, - 0x74, 0x35, 0x8c, 0x53, 0xdb, 0x83, 0xdd, 0xc1, 0x68, 0xd0, 0x2d, 0x7f, 0x51, 0x69, 0xd4, 0xbb, - 0x0d, 0x6a, 0x5b, 0xbb, 0x8e, 0xe5, 0xc4, 0xc6, 0x10, 0x20, 0x2b, 0x52, 0xd1, 0x2b, 0x67, 0x87, - 0x53, 0x3d, 0xa9, 0x38, 0x39, 0xd6, 0x5a, 0x6a, 0x90, 0xda, 0x55, 0xa5, 0xb0, 0xa4, 0x1b, 0x1b, - 0xa0, 0xef, 0xf1, 0xe0, 0x73, 0xf9, 0x42, 0x73, 0x0f, 0x3a, 0x01, 0x0f, 0x63, 0x27, 0xc9, 0xee, - 0xa5, 0xb3, 0x6c, 0x99, 0xed, 0x14, 0x8b, 0xbe, 0xd7, 0x38, 0x82, 0xc6, 0x1e, 0x0f, 0x2e, 0x15, - 0x88, 0xad, 0xb4, 0x31, 0x3c, 0x53, 0xef, 0x47, 0x2a, 0x31, 0xba, 0x07, 0x75, 0x15, 0x4c, 0x94, - 0x3f, 0x2a, 0x04, 0x9a, 0x84, 0x66, 0xfc, 0x4d, 0x09, 0x6e, 0xed, 0xf9, 0xe7, 0x22, 0xcd, 0x59, - 0x0f, 0xf9, 0x85, 0xeb, 0x73, 0xfb, 0x35, 0xda, 0x8d, 0x55, 0x8f, 0x3f, 0xa3, 0x27, 0x9a, 0xe4, - 0xd9, 0xca, 0xd4, 0x25, 0xe6, 0xa9, 0x7a, 0x37, 0x17, 0x51, 0x4c, 0x44, 0x15, 0x82, 0x11, 0x46, - 0xd2, 0x0f, 0xa0, 0x16, 0xcf, 0xbd, 0xec, 0x95, 0xac, 0x1a, 0x53, 0x23, 0x76, 0x69, 0xc2, 0x5a, - 0x5d, 0x9e, 0xb0, 0x1a, 0x8f, 0x41, 0x1f, 0xcd, 0xa9, 0x49, 0x39, 0x8b, 0x0a, 0xa9, 0x51, 0xe9, - 0x15, 0xa9, 0x91, 0xb6, 0x90, 0x1a, 0xfd, 0x7b, 0x09, 0x9a, 0xb9, 0xcc, 0x9b, 0xbd, 0x0d, 0x95, - 0x78, 0xee, 0x15, 0xdf, 0xa2, 0x93, 0x4d, 0x4c, 0x22, 0xa1, 0xc6, 0x4f, 0xf9, 0x7c, 0xcc, 0xa3, - 0xc8, 0x39, 0xf1, 0x84, 0xad, 0x96, 0x6c, 0x4e, 0xf9, 0x7c, 0x53, 0xa1, 0xd8, 0x2e, 0x5c, 0x97, - 0x0e, 0x3d, 0xf9, 0x88, 0xa4, 0x83, 0xf2, 0xce, 0x42, 0xa6, 0x2f, 0x1b, 0xb9, 0xc9, 0x27, 0xa9, - 0xb6, 0x40, 0xe7, 0xa4, 0x80, 0xec, 0x6f, 0xc2, 0xcd, 0x25, 0x6c, 0xdf, 0xab, 0x75, 0xbf, 0x02, - 0xed, 0xd1, 0xdc, 0x1b, 0x39, 0x53, 0x11, 0xc5, 0x7c, 0x1a, 0x50, 0x6a, 0xa9, 0x02, 0x72, 0xc5, - 0xd4, 0xe2, 0xc8, 0x78, 0x0f, 0x5a, 0x87, 0x42, 0x84, 0xa6, 0x88, 0x02, 0xdf, 0x93, 0x69, 0x95, - 0x6a, 0xa0, 0xca, 0xe8, 0xaf, 0x20, 0xe3, 0xb7, 0x40, 0x37, 0xf9, 0x71, 0xbc, 0xc5, 0x63, 0xeb, - 0xf4, 0xfb, 0xf4, 0x08, 0xde, 0x83, 0x7a, 0x20, 0x75, 0x4a, 0x55, 0x68, 0x2d, 0xca, 0x02, 0x94, - 0x9e, 0x99, 0x09, 0xd1, 0xf8, 0x18, 0x6e, 0x0e, 0x67, 0x93, 0xc8, 0x0a, 0x1d, 0xaa, 0x66, 0x93, - 0x08, 0xd9, 0x87, 0x46, 0x10, 0x8a, 0x63, 0x67, 0x2e, 0x12, 0xc3, 0x48, 0x61, 0xe3, 0x27, 0x70, - 0xab, 0x38, 0x45, 0x7d, 0xc2, 0x3b, 0x50, 0x3e, 0x3b, 0x8f, 0xd4, 0xc9, 0x6e, 0x14, 0x8a, 0x13, - 0x7a, 0x02, 0x46, 0xaa, 0x61, 0x42, 0x79, 0x7f, 0x36, 0xcd, 0xff, 0x8d, 0xa5, 0x22, 0xff, 0xc6, - 0x72, 0x27, 0xdf, 0xcf, 0x94, 0xf5, 0x4b, 0xd6, 0xb7, 0x7c, 0x13, 0xf4, 0x63, 0x3f, 0xfc, 0x6d, - 0x1e, 0xda, 0xc2, 0x56, 0xa1, 0x30, 0x43, 0x18, 0x5f, 0x43, 0x33, 0xd1, 0x84, 0x1d, 0x9b, 0xde, - 0xbc, 0x48, 0x15, 0x77, 0xec, 0x82, 0x66, 0xca, 0x6e, 0xa1, 0xf0, 0xec, 0x9d, 0x44, 0x85, 0x24, - 0x50, 0xdc, 0x59, 0x3d, 0x55, 0x24, 0x3b, 0x1b, 0x4f, 0xa0, 0x95, 0x94, 0x7f, 0x7b, 0x22, 0xe6, - 0xa4, 0xdc, 0xae, 0x23, 0xbc, 0x9c, 0xe2, 0x37, 0x24, 0x62, 0x54, 0x6c, 0xfa, 0x69, 0x85, 0xbc, - 0xc2, 0x58, 0x87, 0x9a, 0xb2, 0x1c, 0x06, 0x15, 0xcb, 0xb7, 0xa5, 0x75, 0x57, 0x4d, 0x1a, 0xa3, - 0x38, 0xa6, 0xd1, 0x49, 0x92, 0x33, 0x4d, 0xa3, 0x13, 0xe3, 0xef, 0x34, 0x68, 0x6f, 0x51, 0x33, - 0x24, 0xb9, 0x92, 0x5c, 0x7f, 0xa7, 0x54, 0xe8, 0xef, 0xe4, 0x7b, 0x39, 0x5a, 0xa1, 0x97, 0x53, - 0x38, 0x50, 0xb9, 0x98, 0xe8, 0xfc, 0x10, 0xea, 0x33, 0xcf, 0x99, 0x27, 0x2e, 0x41, 0x37, 0x6b, - 0x08, 0x8e, 0x22, 0xb6, 0x0a, 0x4d, 0xf4, 0x1a, 0x8e, 0x27, 0xbb, 0x36, 0xb2, 0xf5, 0x92, 0x47, - 0x2d, 0xf4, 0x66, 0x6a, 0xaf, 0xee, 0xcd, 0xd4, 0x5f, 0xdb, 0x9b, 0x69, 0xbc, 0xae, 0x37, 0xa3, - 0x2f, 0xf6, 0x66, 0x8a, 0x49, 0x1a, 0x2c, 0x26, 0x69, 0xc6, 0x9f, 0x69, 0xd0, 0x1e, 0xcc, 0x03, - 0xfa, 0x6f, 0xc2, 0x6b, 0x33, 0xbe, 0x9c, 0x5c, 0xb5, 0x82, 0x5c, 0x73, 0x12, 0x2a, 0xab, 0xc7, - 0x08, 0x29, 0x21, 0xcc, 0x01, 0x65, 0xa7, 0x44, 0x49, 0x4e, 0x42, 0xff, 0x07, 0x24, 0x67, 0xec, - 0x42, 0x27, 0x11, 0x8c, 0xb2, 0xda, 0xef, 0xa4, 0x8e, 0xf2, 0x7f, 0x45, 0x6e, 0xda, 0x3f, 0x90, - 0x80, 0xf1, 0x47, 0x1a, 0xe8, 0x52, 0x49, 0xf1, 0x78, 0x1f, 0xa8, 0xfc, 0xb5, 0x94, 0x75, 0x4b, - 0x53, 0xe2, 0xfa, 0x33, 0x71, 0x41, 0x79, 0x97, 0x4c, 0x6b, 0x97, 0xbd, 0x17, 0xa8, 0x60, 0x2a, - 0xab, 0x2e, 0x0a, 0xa6, 0x77, 0x40, 0x97, 0x31, 0x66, 0xe6, 0x24, 0x2f, 0x8c, 0x32, 0xe8, 0x1c, - 0x39, 0xf4, 0x77, 0x8c, 0x58, 0x84, 0x53, 0x25, 0x65, 0x1a, 0x17, 0xf3, 0xdb, 0xb6, 0xca, 0xb8, - 0x8c, 0x53, 0xa8, 0xab, 0xdd, 0x31, 0x01, 0x39, 0xda, 0x7f, 0xb6, 0x7f, 0xf0, 0xd5, 0x7e, 0xf7, - 0x5a, 0xda, 0x5f, 0x2e, 0x65, 0x29, 0x8a, 0x96, 0x4f, 0x51, 0xca, 0x88, 0x7f, 0x7c, 0x70, 0xb4, - 0x3f, 0xea, 0x56, 0x58, 0x1b, 0x74, 0x1a, 0x8e, 0xcd, 0xc1, 0xf3, 0x6e, 0x95, 0x0a, 0xee, 0xc7, - 0x9f, 0x0f, 0xf6, 0x36, 0xbb, 0xb5, 0xb4, 0x3b, 0x5d, 0x37, 0x7e, 0xbf, 0x04, 0x37, 0xe4, 0x27, - 0xe7, 0xcb, 0xd3, 0xfc, 0x7f, 0xfa, 0x2a, 0xf2, 0x3f, 0x7d, 0xbf, 0xe6, 0x8a, 0xf4, 0x1f, 0x4a, - 0xd0, 0x97, 0xc9, 0xcf, 0xd3, 0x90, 0x07, 0xa7, 0x5f, 0xee, 0x5e, 0x2a, 0x7f, 0xae, 0x8a, 0xdd, - 0xf7, 0xa0, 0x43, 0x7f, 0x6c, 0xfc, 0x85, 0x3b, 0x56, 0x29, 0xba, 0xbc, 0xa2, 0xb6, 0xc2, 0xca, - 0x85, 0xd8, 0x27, 0xd0, 0x92, 0x7f, 0x80, 0xa4, 0x5e, 0x5c, 0xe1, 0xb9, 0xa2, 0x90, 0x7a, 0x35, - 0x25, 0x17, 0xa6, 0x39, 0x11, 0xfb, 0x38, 0x9d, 0x94, 0x55, 0x4a, 0x97, 0x5f, 0x24, 0xd4, 0x94, - 0x11, 0xd5, 0x4f, 0x0f, 0xe1, 0xce, 0xd2, 0xef, 0x50, 0xba, 0x9b, 0x6b, 0x4c, 0x49, 0x95, 0xd9, - 0xf8, 0xfb, 0x12, 0x54, 0x30, 0x1e, 0xb2, 0x07, 0xa0, 0x7f, 0x2e, 0x78, 0x18, 0x4f, 0x04, 0x8f, - 0x59, 0x21, 0xf6, 0xf5, 0x69, 0xc7, 0xec, 0xc5, 0xd3, 0xb8, 0xf6, 0xa8, 0xc4, 0xd6, 0xe5, 0x7f, - 0x92, 0x92, 0xbf, 0x5a, 0xb5, 0x93, 0xb8, 0x4a, 0x71, 0xb7, 0x5f, 0x98, 0x6f, 0x5c, 0x5b, 0x23, - 0xfe, 0x2f, 0x7c, 0xc7, 0x7b, 0x2c, 0xff, 0x42, 0xc3, 0x16, 0xe3, 0xf0, 0xe2, 0x0c, 0xf6, 0x00, - 0x6a, 0x3b, 0x11, 0x06, 0xfc, 0xcb, 0xac, 0x24, 0xb5, 0x7c, 0x2e, 0x60, 0x5c, 0xdb, 0xf8, 0xeb, - 0x32, 0x54, 0xbe, 0x16, 0xa1, 0xcf, 0x3e, 0x82, 0xba, 0x7a, 0x1f, 0x66, 0xb9, 0x77, 0xe0, 0x3e, - 0x95, 0x34, 0x0b, 0x0f, 0xc7, 0xb4, 0x4b, 0x57, 0x8a, 0x2b, 0xeb, 0xa0, 0xb2, 0xec, 0xf9, 0xfa, - 0xd2, 0xa1, 0x3e, 0x85, 0xee, 0x30, 0x0e, 0x05, 0x9f, 0xe6, 0xd8, 0x8b, 0xa2, 0x5a, 0xd6, 0x8e, - 0x25, 0x79, 0xdd, 0x87, 0x9a, 0xcc, 0xaa, 0x16, 0x26, 0x2c, 0x76, 0x56, 0x89, 0xf9, 0x7d, 0x68, - 0x0e, 0x4f, 0xfd, 0x99, 0x6b, 0x0f, 0x45, 0x78, 0x2e, 0x58, 0xee, 0x1f, 0x1f, 0xfd, 0xdc, 0xd8, - 0xb8, 0xc6, 0xd6, 0x00, 0x64, 0x20, 0x3f, 0x42, 0x1b, 0xa9, 0x23, 0x6d, 0x7f, 0x36, 0x95, 0x8b, - 0xe6, 0x22, 0xbc, 0xe4, 0xcc, 0x25, 0x57, 0xaf, 0xe2, 0xfc, 0x04, 0xda, 0x8f, 0xc9, 0x5e, 0x0e, - 0xc2, 0xcd, 0x89, 0x1f, 0xc6, 0x6c, 0xf1, 0x5f, 0x1f, 0xfd, 0x45, 0x84, 0x71, 0x8d, 0x3d, 0x82, - 0xc6, 0x28, 0xbc, 0x90, 0xfc, 0x37, 0x54, 0x4e, 0x9a, 0xed, 0xb7, 0xe4, 0x2b, 0x37, 0xfe, 0xb3, - 0x02, 0xb5, 0xaf, 0xfc, 0xf0, 0x4c, 0x84, 0x58, 0xde, 0x52, 0x27, 0x5c, 0xa9, 0x51, 0xda, 0x15, - 0x5f, 0xb6, 0xd1, 0xbb, 0xa0, 0x93, 0x50, 0x46, 0x3c, 0x3a, 0x93, 0x57, 0x45, 0xff, 0xa4, 0x95, - 0x72, 0x91, 0xe5, 0x32, 0xdd, 0x6b, 0x47, 0x5e, 0x54, 0xfa, 0x5a, 0x54, 0xe8, 0x4b, 0xf7, 0xe9, - 0xfb, 0x9f, 0x3d, 0x1f, 0xa2, 0x6a, 0x3e, 0x2a, 0xa1, 0x23, 0x1e, 0xca, 0x2f, 0x45, 0xa6, 0xec, - 0x1f, 0x84, 0x52, 0xf3, 0xb3, 0xbf, 0xec, 0x19, 0xd7, 0xd8, 0x43, 0xa8, 0x29, 0x93, 0xbe, 0x91, - 0x19, 0xaf, 0xf2, 0x13, 0xfd, 0x6e, 0x1e, 0xa5, 0x26, 0x7c, 0x00, 0x35, 0xe9, 0xe1, 0xe4, 0x84, - 0x42, 0x8a, 0x22, 0x4f, 0x2d, 0xd3, 0x1c, 0xe3, 0x1a, 0xbb, 0x0f, 0x75, 0xd5, 0xcd, 0x66, 0x4b, - 0x5a, 0xdb, 0x0b, 0xcc, 0x1f, 0x43, 0x4d, 0x06, 0x26, 0xb9, 0x6e, 0x21, 0x7a, 0xf7, 0x59, 0x1e, - 0x95, 0x18, 0x09, 0x6a, 0xbb, 0x29, 0x2c, 0xe1, 0xe4, 0xca, 0x28, 0x96, 0x48, 0x62, 0x89, 0xc9, - 0x7e, 0x0a, 0xed, 0x42, 0xc9, 0xc5, 0x7a, 0x74, 0x3b, 0x4b, 0xaa, 0xb0, 0x4b, 0x86, 0xf2, 0x13, - 0xd0, 0x55, 0xc6, 0x3b, 0x11, 0x8c, 0xfa, 0xd3, 0x4b, 0x72, 0xe6, 0xfe, 0xe5, 0x94, 0x97, 0xb4, - 0xff, 0xa7, 0x70, 0x73, 0x89, 0x0f, 0x63, 0xf4, 0x37, 0x9b, 0xab, 0x9d, 0x74, 0x7f, 0xe5, 0x4a, - 0x7a, 0x22, 0x80, 0xad, 0xee, 0x3f, 0x7e, 0x7b, 0xb7, 0xf4, 0xcf, 0xdf, 0xde, 0x2d, 0xfd, 0xeb, - 0xb7, 0x77, 0x4b, 0xbf, 0xfc, 0xb7, 0xbb, 0xd7, 0x26, 0x35, 0xfa, 0x37, 0xf9, 0x27, 0xff, 0x1b, - 0x00, 0x00, 0xff, 0xff, 0x14, 0xf8, 0xfd, 0x3e, 0xc3, 0x2e, 0x00, 0x00, + // 4667 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x3a, 0x4d, 0x6f, 0x1c, 0x47, + 0x76, 0x9a, 0x9e, 0xcf, 0x7e, 0xf3, 0xa1, 0x51, 0x49, 0xab, 0x9d, 0x1d, 0xd9, 0x22, 0xdd, 0xb6, + 0x6c, 0xda, 0xb2, 0x28, 0x99, 0xde, 0x20, 0x6b, 0x2f, 0x02, 0x84, 0x14, 0x87, 0x32, 0x2d, 0x8a, + 0xa4, 0x7b, 0x86, 0xf2, 0xae, 0x0f, 0x19, 0xd4, 0x74, 0x17, 0xc9, 0x5e, 0xf6, 0x74, 0xf7, 0x76, + 0xf7, 0x70, 0x87, 0x3e, 0x25, 0x08, 0x92, 0x53, 0x72, 0x49, 0x10, 0x64, 0x4f, 0x49, 0xfe, 0x41, + 0x80, 0x9c, 0x82, 0x9c, 0x83, 0x20, 0xc8, 0x21, 0xc8, 0x2f, 0x50, 0x02, 0x27, 0x27, 0x01, 0x39, + 0x04, 0x01, 0x72, 0x0c, 0x82, 0xf7, 0xaa, 0xfa, 0x6b, 0x38, 0x94, 0xec, 0x05, 0xf6, 0x90, 0x53, + 0xd7, 0x7b, 0xaf, 0x3e, 0x5f, 0xbd, 0xef, 0x6a, 0x68, 0x04, 0x93, 0xf5, 0x20, 0xf4, 0x63, 0x9f, + 0x69, 0xc1, 0xa4, 0xaf, 0xf3, 0xc0, 0x91, 0x60, 0xff, 0x83, 0x13, 0x27, 0x3e, 0x9d, 0x4d, 0xd6, + 0x2d, 0x7f, 0xfa, 0xd0, 0x3e, 0x09, 0x79, 0x70, 0xfa, 0xc0, 0xf1, 0x1f, 0x4e, 0xb8, 0x7d, 0x22, + 0xc2, 0x87, 0xe7, 0x1b, 0x0f, 0x83, 0xc9, 0xc3, 0x64, 0x68, 0xff, 0x41, 0xae, 0xef, 0x89, 0x7f, + 0xe2, 0x3f, 0x24, 0xf4, 0x64, 0x76, 0x4c, 0x10, 0x01, 0xd4, 0x92, 0xdd, 0x8d, 0x3e, 0x54, 0xf6, + 0x9c, 0x28, 0x66, 0x0c, 0x2a, 0x33, 0xc7, 0x8e, 0x7a, 0xa5, 0xd5, 0xf2, 0x5a, 0xcd, 0xa4, 0xb6, + 0xf1, 0x0c, 0xf4, 0x11, 0x8f, 0xce, 0x9e, 0x73, 0x77, 0x26, 0x58, 0x17, 0xca, 0xe7, 0xdc, 0xed, + 0x95, 0x56, 0x4b, 0x6b, 0x2d, 0x13, 0x9b, 0x6c, 0x1d, 0x1a, 0xe7, 0xdc, 0x1d, 0xc7, 0x17, 0x81, + 0xe8, 0x69, 0xab, 0xa5, 0xb5, 0xce, 0xc6, 0xcd, 0xf5, 0x60, 0xb2, 0x7e, 0xe8, 0x47, 0xb1, 0xe3, + 0x9d, 0xac, 0x3f, 0xe7, 0xee, 0xe8, 0x22, 0x10, 0x66, 0xfd, 0x5c, 0x36, 0x8c, 0x03, 0x68, 0x0e, + 0x43, 0x6b, 0x67, 0xe6, 0x59, 0xb1, 0xe3, 0x7b, 0xb8, 0xa2, 0xc7, 0xa7, 0x82, 0x66, 0xd4, 0x4d, + 0x6a, 0x23, 0x8e, 0x87, 0x27, 0x51, 0xaf, 0xbc, 0x5a, 0x46, 0x1c, 0xb6, 0x59, 0x0f, 0xea, 0x4e, + 0xf4, 0xd8, 0x9f, 0x79, 0x71, 0xaf, 0xb2, 0x5a, 0x5a, 0x6b, 0x98, 0x09, 0x68, 0xfc, 0x65, 0x19, + 0xaa, 0x5f, 0xcc, 0x44, 0x78, 0x41, 0xe3, 0xe2, 0x38, 0x4c, 0xe6, 0xc2, 0x36, 0xbb, 0x05, 0x55, + 0x97, 0x7b, 0x27, 0x51, 0x4f, 0xa3, 0xc9, 0x24, 0xc0, 0xee, 0x80, 0xce, 0x8f, 0x63, 0x11, 0x8e, + 0x67, 0x8e, 0xdd, 0x2b, 0xaf, 0x96, 0xd6, 0x6a, 0x66, 0x83, 0x10, 0x47, 0x8e, 0xcd, 0x7e, 0x00, + 0x0d, 0xdb, 0x1f, 0x5b, 0xf9, 0xb5, 0x6c, 0x9f, 0xd6, 0x62, 0x6f, 0x43, 0x63, 0xe6, 0xd8, 0x63, + 0xd7, 0x89, 0xe2, 0x5e, 0x75, 0xb5, 0xb4, 0xd6, 0xdc, 0x68, 0xe0, 0x61, 0x91, 0x77, 0x66, 0x7d, + 0xe6, 0xd8, 0xc4, 0xc4, 0x0f, 0xa0, 0x11, 0x85, 0xd6, 0xf8, 0x78, 0xe6, 0x59, 0xbd, 0x1a, 0x75, + 0xba, 0x8e, 0x9d, 0x72, 0xa7, 0x36, 0xeb, 0x91, 0x04, 0xf0, 0x58, 0xa1, 0x38, 0x17, 0x61, 0x24, + 0x7a, 0x75, 0xb9, 0x94, 0x02, 0xd9, 0x23, 0x68, 0x1e, 0x73, 0x4b, 0xc4, 0xe3, 0x80, 0x87, 0x7c, + 0xda, 0x6b, 0x64, 0x13, 0xed, 0x20, 0xfa, 0x10, 0xb1, 0x91, 0x09, 0xc7, 0x29, 0xc0, 0x3e, 0x86, + 0x36, 0x41, 0xd1, 0xf8, 0xd8, 0x71, 0x63, 0x11, 0xf6, 0x74, 0x1a, 0xd3, 0xa1, 0x31, 0x84, 0x19, + 0x85, 0x42, 0x98, 0x2d, 0xd9, 0x49, 0x62, 0xd8, 0x9b, 0x00, 0x62, 0x1e, 0x70, 0xcf, 0x1e, 0x73, + 0xd7, 0xed, 0x01, 0xed, 0x41, 0x97, 0x98, 0x4d, 0xd7, 0x65, 0xdf, 0xc7, 0xfd, 0x71, 0x7b, 0x1c, + 0x47, 0xbd, 0xf6, 0x6a, 0x69, 0xad, 0x62, 0xd6, 0x10, 0x1c, 0x45, 0xc8, 0x57, 0x8b, 0x5b, 0xa7, + 0xa2, 0xd7, 0x59, 0x2d, 0xad, 0x55, 0x4d, 0x09, 0x20, 0xf6, 0xd8, 0x09, 0xa3, 0xb8, 0x77, 0x5d, + 0x62, 0x09, 0x30, 0x36, 0x40, 0x27, 0xe9, 0x21, 0xee, 0xdc, 0x83, 0xda, 0x39, 0x02, 0x52, 0xc8, + 0x9a, 0x1b, 0x6d, 0xdc, 0x5e, 0x2a, 0x60, 0xa6, 0x22, 0x1a, 0x77, 0xa1, 0xb1, 0xc7, 0xbd, 0x93, + 0x44, 0x2a, 0xf1, 0xda, 0x68, 0x80, 0x6e, 0x52, 0xdb, 0xf8, 0xa5, 0x06, 0x35, 0x53, 0x44, 0x33, + 0x37, 0x66, 0xef, 0x01, 0xe0, 0xa5, 0x4c, 0x79, 0x1c, 0x3a, 0x73, 0x35, 0x6b, 0x76, 0x2d, 0xfa, + 0xcc, 0xb1, 0x9f, 0x11, 0x89, 0x3d, 0x82, 0x16, 0xcd, 0x9e, 0x74, 0xd5, 0xb2, 0x0d, 0xa4, 0xfb, + 0x33, 0x9b, 0xd4, 0x45, 0x8d, 0xb8, 0x0d, 0x35, 0x92, 0x03, 0x29, 0x8b, 0x6d, 0x53, 0x41, 0xec, + 0x1e, 0x74, 0x1c, 0x2f, 0xc6, 0x7b, 0xb2, 0xe2, 0xb1, 0x2d, 0xa2, 0x44, 0x50, 0xda, 0x29, 0x76, + 0x5b, 0x44, 0x31, 0xfb, 0x08, 0x24, 0xb3, 0x93, 0x05, 0xab, 0xb4, 0x60, 0x27, 0xbd, 0xc4, 0x48, + 0xae, 0x48, 0x7d, 0xd4, 0x8a, 0x0f, 0xa0, 0x89, 0xe7, 0x4b, 0x46, 0xd4, 0x68, 0x44, 0x8b, 0x4e, + 0xa3, 0xd8, 0x61, 0x02, 0x76, 0x50, 0xdd, 0x91, 0x35, 0x28, 0x8c, 0x52, 0x78, 0xa8, 0x6d, 0x0c, + 0xa0, 0x7a, 0x10, 0xda, 0x22, 0x5c, 0xaa, 0x0f, 0x0c, 0x2a, 0xb6, 0x88, 0x2c, 0x52, 0xd5, 0x86, + 0x49, 0xed, 0x4c, 0x47, 0xca, 0x39, 0x1d, 0x31, 0xfe, 0xa2, 0x04, 0xcd, 0xa1, 0x1f, 0xc6, 0xcf, + 0x44, 0x14, 0xf1, 0x13, 0xc1, 0x56, 0xa0, 0xea, 0xe3, 0xb4, 0x8a, 0xc3, 0x3a, 0xee, 0x89, 0xd6, + 0x31, 0x25, 0x7e, 0xe1, 0x1e, 0xb4, 0xab, 0xef, 0x01, 0x65, 0x87, 0xb4, 0xab, 0xac, 0x64, 0x87, + 0x74, 0xeb, 0x36, 0xd4, 0xfc, 0xe3, 0xe3, 0x48, 0x48, 0x5e, 0x56, 0x4d, 0x05, 0x5d, 0x29, 0x82, + 0xc6, 0x6f, 0x00, 0xe0, 0xfe, 0xbe, 0xa3, 0x14, 0x18, 0xa7, 0xd0, 0x34, 0xf9, 0x71, 0xfc, 0xd8, + 0xf7, 0x62, 0x31, 0x8f, 0x59, 0x07, 0x34, 0xc7, 0x26, 0x16, 0xd5, 0x4c, 0xcd, 0xb1, 0x71, 0x73, + 0x27, 0xa1, 0x3f, 0x0b, 0x88, 0x43, 0x6d, 0x53, 0x02, 0xc4, 0x4a, 0xdb, 0x0e, 0x69, 0xc7, 0xc8, + 0x4a, 0xdb, 0x0e, 0xd9, 0x0a, 0x34, 0x23, 0x8f, 0x07, 0xd1, 0xa9, 0x1f, 0xe3, 0xe6, 0x2a, 0xb4, + 0x39, 0x48, 0x50, 0xa3, 0xc8, 0xf8, 0x4f, 0x0d, 0x6a, 0xcf, 0xc4, 0x74, 0x22, 0xc2, 0x4b, 0xab, + 0x3c, 0x82, 0x06, 0x4d, 0x3c, 0x76, 0x6c, 0xb9, 0xd0, 0xd6, 0xf7, 0x5e, 0xbe, 0x58, 0xb9, 0x41, + 0xb8, 0x5d, 0xfb, 0x43, 0x7f, 0xea, 0xc4, 0x62, 0x1a, 0xc4, 0x17, 0x66, 0x5d, 0xa1, 0x96, 0xee, + 0xe0, 0x36, 0xd4, 0x5c, 0xc1, 0xf1, 0x4e, 0xa4, 0xf8, 0x29, 0x88, 0x3d, 0x80, 0x3a, 0x9f, 0x8e, + 0x6d, 0xc1, 0x6d, 0xb2, 0x52, 0x8d, 0xad, 0x5b, 0x2f, 0x5f, 0xac, 0x74, 0xf9, 0x74, 0x5b, 0xf0, + 0xfc, 0xdc, 0x35, 0x89, 0x61, 0x9f, 0xa0, 0xcc, 0x45, 0xf1, 0x78, 0x16, 0xd8, 0x3c, 0x16, 0x64, + 0xb3, 0x2a, 0x5b, 0xbd, 0x97, 0x2f, 0x56, 0x6e, 0x21, 0xfa, 0x88, 0xb0, 0xb9, 0x61, 0x90, 0x61, + 0xd9, 0x2e, 0xdc, 0xb0, 0xdc, 0x59, 0x84, 0xa6, 0xd4, 0xf1, 0x8e, 0xfd, 0xb1, 0xef, 0xb9, 0x17, + 0x74, 0x4d, 0x8d, 0xad, 0x37, 0x5f, 0xbe, 0x58, 0xf9, 0x81, 0x22, 0xee, 0x7a, 0xc7, 0xfe, 0x81, + 0xe7, 0x5e, 0xe4, 0x66, 0xb9, 0xbe, 0x40, 0x62, 0xbf, 0x0d, 0x9d, 0x63, 0x3f, 0xb4, 0xc4, 0x38, + 0x65, 0x4c, 0x87, 0xe6, 0xe9, 0xbf, 0x7c, 0xb1, 0x72, 0x9b, 0x28, 0x4f, 0x2e, 0x71, 0xa7, 0x95, + 0xc7, 0x1b, 0x7f, 0xab, 0x41, 0x95, 0xda, 0xec, 0x11, 0xd4, 0xa7, 0xc4, 0xf8, 0xc4, 0xca, 0xdc, + 0x46, 0x49, 0x20, 0xda, 0xba, 0xbc, 0x91, 0x68, 0xe0, 0xc5, 0xe1, 0x85, 0x99, 0x74, 0xc3, 0x11, + 0x31, 0x9f, 0xb8, 0x22, 0x8e, 0x94, 0xe4, 0xe6, 0x46, 0x8c, 0x24, 0x41, 0x8d, 0x50, 0xdd, 0x16, + 0xaf, 0xbf, 0xbc, 0x78, 0xfd, 0xac, 0x0f, 0x0d, 0xeb, 0x54, 0x58, 0x67, 0xd1, 0x6c, 0xaa, 0x84, + 0x23, 0x85, 0xfb, 0x3b, 0xd0, 0xca, 0xef, 0x03, 0xfd, 0xea, 0x99, 0xb8, 0x20, 0x01, 0xa9, 0x98, + 0xd8, 0x64, 0xab, 0x50, 0x25, 0x4b, 0x44, 0xe2, 0xd1, 0xdc, 0x00, 0xdc, 0x8e, 0x1c, 0x62, 0x4a, + 0xc2, 0xa7, 0xda, 0x8f, 0x4a, 0x38, 0x4f, 0x7e, 0x77, 0xf9, 0x79, 0xf4, 0xab, 0xe7, 0x91, 0x43, + 0x72, 0xf3, 0x18, 0x3e, 0xd4, 0xf7, 0x1c, 0x4b, 0x78, 0x11, 0x79, 0xdf, 0x59, 0x24, 0x52, 0xab, + 0x81, 0x6d, 0x3c, 0xca, 0x94, 0xcf, 0xf7, 0x7d, 0x5b, 0x44, 0x34, 0x4f, 0xc5, 0x4c, 0x61, 0xa4, + 0x89, 0x79, 0xe0, 0x84, 0x17, 0x23, 0xc9, 0x84, 0xb2, 0x99, 0xc2, 0xe8, 0xde, 0x84, 0x87, 0x8b, + 0xd9, 0x89, 0x27, 0x55, 0xa0, 0xf1, 0x57, 0x65, 0x68, 0x7d, 0x25, 0x42, 0xff, 0x30, 0xf4, 0x03, + 0x3f, 0xe2, 0x2e, 0xdb, 0x2c, 0xb2, 0x53, 0x5e, 0xdb, 0x2a, 0xee, 0x36, 0xdf, 0x6d, 0x7d, 0x98, + 0xf2, 0x57, 0x5e, 0x47, 0x9e, 0xe1, 0x06, 0xd4, 0xe4, 0x75, 0x2e, 0xe1, 0x99, 0xa2, 0x60, 0x1f, + 0x79, 0x81, 0xb4, 0xd7, 0x22, 0x3f, 0x14, 0x85, 0xdd, 0x05, 0x98, 0xf2, 0xf9, 0x9e, 0xe0, 0x91, + 0xd8, 0xb5, 0x13, 0xbd, 0xce, 0x30, 0x8a, 0x1b, 0xa3, 0xb9, 0x37, 0x8a, 0x48, 0xbf, 0x24, 0x37, + 0x08, 0x66, 0x6f, 0x80, 0x3e, 0xe5, 0x73, 0x34, 0x30, 0xbb, 0xb6, 0xd4, 0x24, 0x33, 0x43, 0xb0, + 0xb7, 0xa0, 0x1c, 0xcf, 0x3d, 0xb2, 0xd6, 0xe8, 0xcc, 0x31, 0xb6, 0x1b, 0xcd, 0x3d, 0x65, 0x8a, + 0x4c, 0xa4, 0x25, 0x37, 0xd8, 0xc8, 0x6e, 0xb0, 0x0b, 0x65, 0xcb, 0xb1, 0xc9, 0x9b, 0xeb, 0x26, + 0x36, 0xd9, 0x3d, 0xa8, 0xbb, 0xf2, 0xb6, 0xc8, 0x63, 0x37, 0x37, 0x9a, 0xd2, 0xd0, 0x11, 0xca, + 0x4c, 0x68, 0xfd, 0xdf, 0x82, 0xeb, 0x0b, 0xec, 0xca, 0xcb, 0x47, 0x5b, 0xce, 0x7e, 0x2b, 0x2f, + 0x1f, 0x95, 0xbc, 0x4c, 0xfc, 0x6b, 0x19, 0xae, 0x2b, 0x21, 0x3d, 0x75, 0x82, 0x61, 0x8c, 0xfa, + 0xde, 0x83, 0x3a, 0x59, 0x6b, 0x25, 0x1f, 0x15, 0x33, 0x01, 0xd9, 0x6f, 0x42, 0x8d, 0x14, 0x37, + 0xd1, 0x9f, 0x95, 0x8c, 0xf9, 0xe9, 0x70, 0xa9, 0x4f, 0xea, 0xe6, 0x54, 0x77, 0xf6, 0x43, 0xa8, + 0x7e, 0x2d, 0x42, 0x5f, 0x7a, 0x9f, 0xe6, 0xc6, 0xdd, 0x65, 0xe3, 0x50, 0x04, 0xd4, 0x30, 0xd9, + 0xf9, 0xd7, 0x78, 0x47, 0xef, 0xa0, 0xbf, 0x99, 0xfa, 0xe7, 0xc2, 0xee, 0xd5, 0x69, 0x47, 0x79, + 0x31, 0x4a, 0x48, 0xc9, 0xa5, 0x34, 0x96, 0x5e, 0x8a, 0xfe, 0x8a, 0x4b, 0xd9, 0x86, 0x66, 0x8e, + 0x0b, 0x4b, 0x2e, 0x64, 0xa5, 0xa8, 0xb0, 0x7a, 0x6a, 0x87, 0xf2, 0x7a, 0xbf, 0x0d, 0x90, 0xf1, + 0xe4, 0x57, 0xb5, 0x1e, 0xc6, 0xef, 0x95, 0xe0, 0xfa, 0x63, 0xdf, 0xf3, 0x04, 0x45, 0xa5, 0xf2, + 0x86, 0x33, 0x25, 0x2a, 0x5d, 0xa9, 0x44, 0xef, 0x43, 0x35, 0xc2, 0xce, 0x6a, 0xf6, 0x9b, 0x4b, + 0xae, 0xcc, 0x94, 0x3d, 0xd0, 0x4a, 0x4e, 0xf9, 0x7c, 0x1c, 0x08, 0xcf, 0x76, 0xbc, 0x93, 0xc4, + 0x4a, 0x4e, 0xf9, 0xfc, 0x50, 0x62, 0x8c, 0x3f, 0xd3, 0x00, 0x3e, 0x13, 0xdc, 0x8d, 0x4f, 0xd1, + 0x13, 0xe0, 0xbd, 0x39, 0x5e, 0x14, 0x73, 0xcf, 0x4a, 0x72, 0x82, 0x14, 0x46, 0xe1, 0x43, 0xb7, + 0x27, 0x22, 0x69, 0x84, 0x74, 0x33, 0x01, 0xd1, 0x11, 0xe2, 0x72, 0xb3, 0x48, 0xb9, 0x47, 0x05, + 0x65, 0xce, 0xbc, 0x42, 0x68, 0xe5, 0xcc, 0x7b, 0x50, 0xc7, 0x18, 0xdb, 0xf1, 0x3d, 0x12, 0x0d, + 0xdd, 0x4c, 0x40, 0x9c, 0x67, 0x16, 0xc4, 0xce, 0x54, 0x3a, 0xc1, 0xb2, 0xa9, 0x20, 0xdc, 0x15, + 0x3a, 0xbd, 0x81, 0x75, 0xea, 0x93, 0xf2, 0x96, 0xcd, 0x14, 0xc6, 0xd9, 0x7c, 0xef, 0xc4, 0xc7, + 0xd3, 0x35, 0x28, 0x7e, 0x4a, 0x40, 0x79, 0x16, 0x5b, 0xcc, 0x91, 0xa4, 0x13, 0x29, 0x85, 0x91, + 0x2f, 0x42, 0x8c, 0x8f, 0x05, 0x8f, 0x67, 0xa1, 0x88, 0x7a, 0x40, 0x64, 0x10, 0x62, 0x47, 0x61, + 0x8c, 0xdf, 0xd5, 0xa0, 0x26, 0xed, 0x52, 0x21, 0x58, 0x28, 0x7d, 0xab, 0x60, 0xe1, 0x0d, 0xd0, + 0x83, 0x50, 0xd8, 0x8e, 0x95, 0x5c, 0x92, 0x6e, 0x66, 0x08, 0x8a, 0xd2, 0xd1, 0x6f, 0x12, 0xb3, + 0x1a, 0xa6, 0x04, 0x10, 0x1b, 0x05, 0xdc, 0x12, 0xea, 0x80, 0x12, 0x40, 0x8e, 0x48, 0x91, 0x27, + 0x51, 0x6f, 0x98, 0x0a, 0x62, 0x1f, 0x83, 0x4e, 0x51, 0x19, 0x39, 0x7c, 0x9d, 0x1c, 0xf5, 0xed, + 0x97, 0x2f, 0x56, 0x18, 0x22, 0x17, 0x3c, 0x7d, 0x23, 0xc1, 0x61, 0x5c, 0x82, 0x83, 0xd1, 0xbe, + 0x03, 0x05, 0x19, 0x14, 0x97, 0x20, 0x6a, 0x14, 0xe5, 0xe3, 0x12, 0x89, 0x31, 0xfe, 0x59, 0x83, + 0xd6, 0xb6, 0x13, 0x0a, 0x2b, 0x16, 0xf6, 0xc0, 0x3e, 0xa1, 0xcd, 0x08, 0x2f, 0x76, 0xe2, 0x0b, + 0x15, 0x49, 0x29, 0x28, 0x0d, 0x74, 0xb5, 0x62, 0xe2, 0x27, 0x35, 0xa0, 0x4c, 0xb9, 0xaa, 0x04, + 0xd8, 0x06, 0x80, 0x4c, 0x01, 0x28, 0x5f, 0xad, 0x5c, 0x9d, 0xaf, 0xea, 0xd4, 0x0d, 0x9b, 0x98, + 0x0f, 0xca, 0x31, 0x8e, 0x0c, 0xa7, 0x6a, 0x94, 0xcc, 0xce, 0xd0, 0xca, 0x50, 0xe4, 0x3c, 0x11, + 0x2e, 0x89, 0x0b, 0x45, 0xce, 0x13, 0xe1, 0xa6, 0xf9, 0x4a, 0x5d, 0x6e, 0x07, 0xdb, 0xec, 0x6d, + 0xd0, 0xfc, 0x80, 0x78, 0xa8, 0x16, 0xcc, 0x1f, 0x6c, 0xfd, 0x20, 0x30, 0x35, 0x3f, 0x40, 0xdd, + 0x93, 0xc9, 0x19, 0x89, 0x0b, 0xea, 0x1e, 0x7a, 0x08, 0x4a, 0x15, 0x4c, 0x45, 0x61, 0x06, 0xb4, + 0xb8, 0xeb, 0xfa, 0xbf, 0x10, 0xf6, 0x61, 0x28, 0xec, 0x44, 0x72, 0x0a, 0x38, 0xe3, 0x36, 0x68, + 0x07, 0x01, 0xab, 0x43, 0x79, 0x38, 0x18, 0x75, 0xaf, 0x61, 0x63, 0x7b, 0xb0, 0xd7, 0x2d, 0x19, + 0xdf, 0x68, 0xa0, 0x3f, 0x9b, 0xc5, 0x1c, 0xb5, 0x3d, 0xc2, 0x73, 0x15, 0xc5, 0x2a, 0x93, 0x9f, + 0x1f, 0x40, 0x23, 0x8a, 0x79, 0x48, 0x9e, 0x58, 0xfa, 0x85, 0x3a, 0xc1, 0xa3, 0x88, 0xbd, 0x0b, + 0x55, 0x61, 0x9f, 0x88, 0xc4, 0x5c, 0x77, 0x17, 0xcf, 0x62, 0x4a, 0x32, 0x5b, 0x83, 0x5a, 0x64, + 0x9d, 0x8a, 0x29, 0xef, 0x55, 0xb2, 0x8e, 0x43, 0xc2, 0xc8, 0xd8, 0xd1, 0x54, 0x74, 0xf6, 0x0e, + 0x54, 0xf1, 0x36, 0x22, 0x95, 0xec, 0x50, 0x7a, 0x84, 0x8c, 0x57, 0xdd, 0x24, 0x11, 0x65, 0xc7, + 0x0e, 0xfd, 0x60, 0xec, 0x07, 0xc4, 0xd7, 0xce, 0xc6, 0x2d, 0xb2, 0x3a, 0xc9, 0x69, 0xd6, 0xb7, + 0x43, 0x3f, 0x38, 0x08, 0xcc, 0x9a, 0x4d, 0x5f, 0xcc, 0x6b, 0xa9, 0xbb, 0x94, 0x01, 0x69, 0xa6, + 0x75, 0xc4, 0xc8, 0x3a, 0xc6, 0x1a, 0x34, 0xa6, 0x22, 0xe6, 0x36, 0x8f, 0xb9, 0xb2, 0xd6, 0x2d, + 0x69, 0xc4, 0x24, 0xce, 0x4c, 0xa9, 0xc6, 0x43, 0xa8, 0xc9, 0xa9, 0x59, 0x03, 0x2a, 0xfb, 0x07, + 0xfb, 0x03, 0xc9, 0xd0, 0xcd, 0xbd, 0xbd, 0x6e, 0x09, 0x51, 0xdb, 0x9b, 0xa3, 0xcd, 0xae, 0x86, + 0xad, 0xd1, 0x4f, 0x0f, 0x07, 0xdd, 0xb2, 0xf1, 0x4f, 0x25, 0x68, 0x24, 0xf3, 0xb0, 0x4f, 0x01, + 0x50, 0xef, 0xc6, 0xa7, 0x8e, 0x97, 0x06, 0x35, 0x77, 0xf2, 0x2b, 0xad, 0xe3, 0x8d, 0x7d, 0x86, + 0x54, 0xe9, 0xde, 0x48, 0x4d, 0x09, 0xee, 0x0f, 0xa1, 0x53, 0x24, 0x2e, 0x89, 0xee, 0xee, 0xe7, + 0xed, 0x7c, 0x67, 0xe3, 0x7b, 0x85, 0xa9, 0x71, 0x24, 0x09, 0x73, 0xce, 0xe4, 0x3f, 0x80, 0x46, + 0x82, 0x66, 0x4d, 0xa8, 0x6f, 0x0f, 0x76, 0x36, 0x8f, 0xf6, 0x50, 0x48, 0x00, 0x6a, 0xc3, 0xdd, + 0xfd, 0x27, 0x7b, 0x03, 0x79, 0xac, 0xbd, 0xdd, 0xe1, 0xa8, 0xab, 0x19, 0x7f, 0x5a, 0x82, 0x46, + 0x12, 0x43, 0xb0, 0xf7, 0xd1, 0xf9, 0x53, 0xa8, 0xa2, 0x7c, 0x03, 0x95, 0x23, 0x72, 0xc9, 0x94, + 0x99, 0xd0, 0x51, 0x31, 0xc8, 0xd4, 0x25, 0x51, 0x05, 0x01, 0xf9, 0x54, 0xae, 0x5c, 0xa8, 0x26, + 0x60, 0x56, 0xea, 0x7b, 0x42, 0x05, 0x89, 0xd4, 0x26, 0x19, 0x74, 0x3c, 0x8b, 0xac, 0x45, 0x55, + 0xc9, 0x20, 0xc2, 0xa3, 0xc8, 0xf8, 0x93, 0x0a, 0x74, 0x4c, 0x11, 0xc5, 0x7e, 0x28, 0x4c, 0xf1, + 0xf3, 0x19, 0xa6, 0xda, 0xaf, 0x10, 0xe6, 0x37, 0x01, 0x42, 0xd9, 0x39, 0x13, 0x67, 0x5d, 0x61, + 0x64, 0x98, 0xee, 0xfa, 0x16, 0x49, 0x91, 0xf2, 0x1e, 0x29, 0xcc, 0xee, 0x80, 0x3e, 0xe1, 0xd6, + 0x99, 0x9c, 0x56, 0xfa, 0x90, 0x86, 0x44, 0xc8, 0x79, 0xb9, 0x65, 0x89, 0x28, 0x1a, 0xe3, 0xa5, + 0x48, 0x4f, 0xa2, 0x4b, 0xcc, 0x53, 0x71, 0x81, 0xe4, 0x48, 0x58, 0xa1, 0x88, 0x89, 0x2c, 0x0d, + 0x84, 0x2e, 0x31, 0x48, 0x7e, 0x1b, 0xda, 0x91, 0x88, 0xd0, 0xeb, 0x8c, 0x63, 0xff, 0x4c, 0x78, + 0xca, 0x5a, 0xb4, 0x14, 0x72, 0x84, 0x38, 0xb4, 0xe3, 0xdc, 0xf3, 0xbd, 0x8b, 0xa9, 0x3f, 0x8b, + 0x94, 0x01, 0xce, 0x10, 0x6c, 0x1d, 0x6e, 0x0a, 0xcf, 0x0a, 0x2f, 0x02, 0xdc, 0x2b, 0xae, 0x32, + 0x3e, 0x76, 0x5c, 0xa1, 0x02, 0xc5, 0x1b, 0x19, 0xe9, 0xa9, 0xb8, 0xd8, 0x71, 0x5c, 0x81, 0x3b, + 0x3a, 0xe7, 0x33, 0x37, 0x1e, 0x53, 0x22, 0x09, 0x72, 0x47, 0x84, 0xd9, 0xc4, 0x6c, 0xf2, 0x03, + 0xb8, 0x21, 0xc9, 0xa1, 0xef, 0x0a, 0xc7, 0x96, 0x93, 0x35, 0xa9, 0xd7, 0x75, 0x22, 0x98, 0x84, + 0xa7, 0xa9, 0xd6, 0xe1, 0xa6, 0xec, 0x2b, 0x0f, 0x94, 0xf4, 0x6e, 0xc9, 0xa5, 0x89, 0x34, 0x54, + 0x94, 0xe2, 0xd2, 0x01, 0x8f, 0x4f, 0x29, 0x41, 0x4c, 0x96, 0x3e, 0xe4, 0xf1, 0x29, 0x7a, 0x43, + 0x49, 0x3e, 0x76, 0x84, 0x2b, 0x13, 0x3f, 0xdd, 0x94, 0x23, 0x76, 0x10, 0xc3, 0xde, 0x82, 0x96, + 0xea, 0xe0, 0x87, 0x53, 0x2e, 0xeb, 0x4b, 0xba, 0x29, 0x07, 0xed, 0x10, 0xca, 0xf8, 0x5f, 0x0d, + 0x1a, 0x69, 0x36, 0x71, 0x1f, 0xf4, 0x69, 0x62, 0x1a, 0x54, 0x94, 0xd2, 0x2e, 0xd8, 0x0b, 0x33, + 0xa3, 0xb3, 0x37, 0x41, 0x3b, 0x3b, 0x57, 0x66, 0xaa, 0xbd, 0x2e, 0x0b, 0xaa, 0xc1, 0x64, 0x63, + 0xfd, 0xe9, 0x73, 0x53, 0x3b, 0x3b, 0xcf, 0xa2, 0x9d, 0xea, 0x6b, 0xa3, 0x9d, 0xf7, 0xe0, 0xba, + 0xe5, 0x0a, 0xee, 0x8d, 0x33, 0xef, 0x2b, 0x2f, 0xbe, 0x43, 0xe8, 0xc3, 0xd4, 0x05, 0x2b, 0x4d, + 0xae, 0x67, 0x9a, 0x7c, 0x0f, 0xaa, 0xb6, 0x70, 0x63, 0x9e, 0xaf, 0xf4, 0x1d, 0x84, 0xdc, 0x72, + 0xc5, 0x36, 0xa2, 0x4d, 0x49, 0x45, 0xc3, 0x95, 0x64, 0x3c, 0x79, 0xc3, 0x95, 0xe8, 0xa8, 0x99, + 0x52, 0x33, 0x15, 0x84, 0xbc, 0x0a, 0xde, 0x87, 0x1b, 0x62, 0x1e, 0x90, 0xb5, 0x1e, 0xa7, 0xd9, + 0x69, 0x93, 0x7a, 0x74, 0x13, 0xc2, 0x63, 0x85, 0x67, 0x1f, 0xa2, 0xbe, 0x92, 0x9e, 0xd0, 0xcd, + 0x36, 0x37, 0x18, 0x29, 0x7c, 0x41, 0xf3, 0xcc, 0xa4, 0x8b, 0xe1, 0x41, 0xf9, 0xe9, 0xf3, 0xa1, + 0xe2, 0x66, 0xe9, 0x2a, 0x6e, 0x26, 0xaa, 0xae, 0xe5, 0x54, 0xfd, 0xae, 0xb4, 0x92, 0xc4, 0x9a, + 0xa4, 0x0a, 0x95, 0xc3, 0xe0, 0x51, 0xa4, 0x87, 0xa8, 0xc8, 0x02, 0x15, 0x01, 0xc6, 0xff, 0x94, + 0xa1, 0xae, 0xdc, 0x36, 0xf2, 0x73, 0x96, 0x16, 0x58, 0xb0, 0x59, 0xcc, 0x6b, 0x52, 0xff, 0x9f, + 0xaf, 0x56, 0x97, 0x5f, 0x5f, 0xad, 0x66, 0x9f, 0x42, 0x2b, 0x90, 0xb4, 0x7c, 0xc4, 0xf0, 0xfd, + 0xfc, 0x18, 0xf5, 0xa5, 0x71, 0xcd, 0x20, 0x03, 0xd0, 0x24, 0x51, 0x29, 0x2f, 0xe6, 0x27, 0x24, + 0x3a, 0x2d, 0xb3, 0x8e, 0xf0, 0x88, 0x9f, 0x5c, 0x11, 0x37, 0x7c, 0x1b, 0xf7, 0xdf, 0xa1, 0x38, + 0xa2, 0x45, 0x16, 0x0e, 0x43, 0x86, 0xbc, 0xa7, 0x6e, 0x17, 0x3d, 0xf5, 0x1d, 0xd0, 0x2d, 0x7f, + 0x3a, 0x75, 0x88, 0xd6, 0x51, 0x05, 0x08, 0x42, 0x8c, 0x22, 0xe3, 0x0f, 0x4b, 0x50, 0x57, 0xa7, + 0xbd, 0xe4, 0x07, 0xb6, 0x76, 0xf7, 0x37, 0xcd, 0x9f, 0x76, 0x4b, 0xe8, 0xe7, 0x76, 0xf7, 0x47, + 0x5d, 0x8d, 0xe9, 0x50, 0xdd, 0xd9, 0x3b, 0xd8, 0x1c, 0x75, 0xcb, 0xe8, 0x1b, 0xb6, 0x0e, 0x0e, + 0xf6, 0xba, 0x15, 0xd6, 0x82, 0xc6, 0xf6, 0xe6, 0x68, 0x30, 0xda, 0x7d, 0x36, 0xe8, 0x56, 0xb1, + 0xef, 0x93, 0xc1, 0x41, 0xb7, 0x86, 0x8d, 0xa3, 0xdd, 0xed, 0x6e, 0x1d, 0xe9, 0x87, 0x9b, 0xc3, + 0xe1, 0x97, 0x07, 0xe6, 0x76, 0xb7, 0x41, 0xfe, 0x65, 0x64, 0xee, 0xee, 0x3f, 0xe9, 0xea, 0xd8, + 0x3e, 0xd8, 0xfa, 0x7c, 0xf0, 0x78, 0xd4, 0x05, 0xe3, 0x23, 0x68, 0xe6, 0x38, 0x88, 0xa3, 0xcd, + 0xc1, 0x4e, 0xf7, 0x1a, 0x2e, 0xf9, 0x7c, 0x73, 0xef, 0x08, 0xdd, 0x51, 0x07, 0x80, 0x9a, 0xe3, + 0xbd, 0xcd, 0xfd, 0x27, 0x5d, 0xcd, 0xf8, 0x02, 0x1a, 0x47, 0x8e, 0xbd, 0xe5, 0xfa, 0xd6, 0x19, + 0x8a, 0xd3, 0x84, 0x47, 0x42, 0xe5, 0x3e, 0xd4, 0xc6, 0x30, 0x91, 0x94, 0x25, 0x52, 0x77, 0xaf, + 0x20, 0xe4, 0x95, 0x37, 0x9b, 0x8e, 0xe9, 0x85, 0xa3, 0x2c, 0x7d, 0x84, 0x37, 0x9b, 0x1e, 0x39, + 0x76, 0x64, 0xec, 0x43, 0xfd, 0xc8, 0xb1, 0x0f, 0xb9, 0x75, 0x86, 0xa6, 0x6a, 0x82, 0x53, 0x8f, + 0x23, 0xe7, 0x6b, 0xa1, 0x7c, 0x89, 0x4e, 0x98, 0xa1, 0xf3, 0xb5, 0x60, 0xef, 0x40, 0x8d, 0x80, + 0x24, 0xcf, 0x25, 0xf5, 0x4b, 0xb6, 0x63, 0x2a, 0x9a, 0xf1, 0x47, 0xa5, 0xf4, 0x58, 0x54, 0xc2, + 0x5e, 0x81, 0x4a, 0xc0, 0xad, 0x33, 0xe5, 0x37, 0x9b, 0x6a, 0x0c, 0xae, 0x67, 0x12, 0x81, 0xbd, + 0x07, 0x0d, 0x25, 0x3b, 0xc9, 0xc4, 0xcd, 0x9c, 0x90, 0x99, 0x29, 0xb1, 0x78, 0xab, 0xe5, 0xe2, + 0xad, 0x52, 0x1e, 0x14, 0xb8, 0x4e, 0x2c, 0x35, 0xa5, 0x62, 0x2a, 0xc8, 0xf8, 0x21, 0x40, 0xf6, + 0x6a, 0xb0, 0x24, 0x8c, 0xb8, 0x05, 0x55, 0xee, 0x3a, 0x3c, 0xc9, 0xab, 0x24, 0x60, 0xec, 0x43, + 0x33, 0xf7, 0xd6, 0x80, 0xec, 0xe3, 0xae, 0x8b, 0x7e, 0x26, 0xa2, 0xb1, 0x0d, 0xb3, 0xce, 0x5d, + 0xf7, 0xa9, 0xb8, 0x88, 0x30, 0x84, 0x93, 0xcf, 0x14, 0xda, 0x42, 0x85, 0x9b, 0x86, 0x9a, 0x92, + 0x68, 0x7c, 0x08, 0xb5, 0x9d, 0x24, 0x88, 0x4d, 0x24, 0xbd, 0x74, 0x95, 0xa4, 0x1b, 0x9f, 0xa8, + 0x3d, 0x53, 0x91, 0x9c, 0xdd, 0x57, 0xcf, 0x21, 0x91, 0x7c, 0x7c, 0x29, 0x65, 0x99, 0xb9, 0xec, + 0xa4, 0x5e, 0x42, 0xa8, 0xb3, 0xb1, 0x0d, 0x8d, 0x57, 0x3e, 0x30, 0x29, 0x06, 0x68, 0x19, 0x03, + 0x96, 0x3c, 0x39, 0x19, 0x3f, 0x03, 0xc8, 0x9e, 0x4d, 0x94, 0xe2, 0xc9, 0x59, 0x50, 0xf1, 0x3e, + 0x80, 0x86, 0x75, 0xea, 0xb8, 0x76, 0x28, 0xbc, 0xc2, 0xa9, 0xb3, 0x87, 0x96, 0x94, 0xce, 0x56, + 0xa1, 0x42, 0xaf, 0x41, 0xe5, 0xcc, 0x60, 0xa7, 0x4f, 0x41, 0x44, 0x31, 0xe6, 0xd0, 0x96, 0xb1, + 0xf1, 0xb7, 0x88, 0x67, 0x8a, 0xd6, 0x52, 0xbb, 0x64, 0x2d, 0x6f, 0x43, 0x8d, 0xdc, 0x68, 0x72, + 0x1a, 0x05, 0x5d, 0x61, 0x45, 0x7f, 0x5f, 0x03, 0x90, 0x4b, 0xef, 0xfb, 0xb6, 0x28, 0x66, 0x8e, + 0xa5, 0xc5, 0xcc, 0x91, 0x41, 0x25, 0x7d, 0xe8, 0xd3, 0x4d, 0x6a, 0x67, 0x7e, 0x46, 0x65, 0x93, + 0xd2, 0xcf, 0xbc, 0x01, 0x3a, 0x85, 0x35, 0xce, 0xd7, 0x54, 0x9d, 0xc6, 0x05, 0x33, 0x44, 0xfe, + 0xd9, 0xab, 0x5a, 0x7c, 0xf6, 0x4a, 0xdf, 0x06, 0x6a, 0x72, 0x36, 0xf9, 0x36, 0xb0, 0xe4, 0x99, + 0x43, 0xe6, 0xea, 0x91, 0x08, 0xe3, 0x24, 0x33, 0x95, 0x50, 0x9a, 0x7d, 0xe9, 0xaa, 0x2f, 0x97, + 0xd9, 0xb6, 0xe7, 0x8f, 0x2d, 0xdf, 0x3b, 0x76, 0x1d, 0x2b, 0x56, 0xcf, 0x5c, 0xe0, 0xf9, 0x8f, + 0x15, 0xc6, 0xf8, 0x14, 0x5a, 0x09, 0xff, 0xe9, 0x35, 0xe1, 0x83, 0x34, 0x7b, 0x29, 0x65, 0x77, + 0x9b, 0xb1, 0x69, 0x4b, 0xeb, 0x95, 0x92, 0xfc, 0xc5, 0xf8, 0xef, 0x72, 0x32, 0x58, 0x15, 0xc5, + 0x5f, 0xcd, 0xc3, 0x62, 0x0a, 0xaa, 0x7d, 0xab, 0x14, 0xf4, 0x47, 0xa0, 0xdb, 0x94, 0x63, 0x39, + 0xe7, 0x89, 0xdf, 0xea, 0x2f, 0xe6, 0x53, 0x2a, 0x0b, 0x73, 0xce, 0x85, 0x99, 0x75, 0x7e, 0xcd, + 0x3d, 0xa4, 0xdc, 0xae, 0x2e, 0xe3, 0x76, 0xed, 0x57, 0xe4, 0xf6, 0x5b, 0xd0, 0xf2, 0x7c, 0x6f, + 0xec, 0xcd, 0x5c, 0x97, 0x4f, 0x5c, 0xa1, 0xd8, 0xdd, 0xf4, 0x7c, 0x6f, 0x5f, 0xa1, 0x30, 0xd6, + 0xcc, 0x77, 0x91, 0x4a, 0xdd, 0xa4, 0x7e, 0xd7, 0x73, 0xfd, 0x48, 0xf5, 0xd7, 0xa0, 0xeb, 0x4f, + 0x7e, 0x26, 0xac, 0x98, 0x38, 0x36, 0x26, 0x6d, 0x96, 0x81, 0x66, 0x47, 0xe2, 0x91, 0x45, 0xfb, + 0xa8, 0xd7, 0x0b, 0xd7, 0xdc, 0xbe, 0x74, 0xcd, 0x9f, 0x80, 0x9e, 0x72, 0x29, 0x97, 0xcf, 0xe9, + 0x50, 0xdd, 0xdd, 0xdf, 0x1e, 0xfc, 0xa4, 0x5b, 0x42, 0x5f, 0x68, 0x0e, 0x9e, 0x0f, 0xcc, 0xe1, + 0xa0, 0xab, 0xa1, 0x9f, 0xda, 0x1e, 0xec, 0x0d, 0x46, 0x83, 0x6e, 0xf9, 0xf3, 0x4a, 0xa3, 0xde, + 0x6d, 0x50, 0x69, 0xdb, 0x75, 0x2c, 0x27, 0x36, 0x86, 0x00, 0x59, 0x92, 0x8a, 0x56, 0x39, 0xdb, + 0x9c, 0xaa, 0x5b, 0xc5, 0xc9, 0xb6, 0xd6, 0x52, 0x85, 0xd4, 0xae, 0x4a, 0x85, 0x25, 0xdd, 0xd8, + 0x00, 0xfd, 0x19, 0x0f, 0x3e, 0x93, 0xaf, 0x38, 0xf7, 0xa0, 0x13, 0xf0, 0x30, 0x76, 0x92, 0xe8, + 0x5e, 0x1a, 0xcb, 0x96, 0xd9, 0x4e, 0xb1, 0x68, 0x7b, 0x8d, 0x23, 0x68, 0x3c, 0xe3, 0xc1, 0xa5, + 0x04, 0xb1, 0x95, 0x16, 0x8f, 0x67, 0xea, 0x8d, 0x49, 0x05, 0x46, 0xf7, 0xa0, 0xae, 0x9c, 0x89, + 0xb2, 0x47, 0x05, 0x47, 0x93, 0xd0, 0x8c, 0xbf, 0x29, 0xc1, 0xad, 0x67, 0xfe, 0xb9, 0x48, 0x63, + 0xd6, 0x43, 0x7e, 0xe1, 0xfa, 0xdc, 0x7e, 0x8d, 0x74, 0x63, 0xd6, 0xe3, 0xcf, 0xe8, 0x19, 0x27, + 0x79, 0xda, 0x32, 0x75, 0x89, 0x79, 0xa2, 0xde, 0xd6, 0x45, 0x14, 0x13, 0x51, 0xb9, 0x60, 0x84, + 0x91, 0xf4, 0x3d, 0xa8, 0xc5, 0x73, 0x2f, 0x7b, 0x49, 0xab, 0xc6, 0x54, 0xac, 0x5d, 0x1a, 0xb0, + 0x56, 0x97, 0x07, 0xac, 0xc6, 0x63, 0xd0, 0x47, 0x73, 0x2a, 0x64, 0xce, 0xa2, 0x42, 0x68, 0x54, + 0x7a, 0x45, 0x68, 0xa4, 0x2d, 0x84, 0x46, 0xff, 0x51, 0x82, 0x66, 0x2e, 0xf2, 0x66, 0x6f, 0x41, + 0x25, 0x9e, 0x7b, 0xc5, 0xf7, 0xea, 0x64, 0x11, 0x93, 0x48, 0x28, 0xf1, 0x53, 0x3e, 0x1f, 0xf3, + 0x28, 0x72, 0x4e, 0x3c, 0x61, 0xab, 0x29, 0x9b, 0x53, 0x3e, 0xdf, 0x54, 0x28, 0xb6, 0x07, 0xd7, + 0xa5, 0x41, 0x4f, 0x0e, 0x91, 0x54, 0x50, 0xde, 0x5e, 0x88, 0xf4, 0x65, 0xb1, 0x37, 0x39, 0x92, + 0x2a, 0x0b, 0x74, 0x4e, 0x0a, 0xc8, 0xfe, 0x26, 0xdc, 0x5c, 0xd2, 0xed, 0x3b, 0x95, 0xf7, 0x57, + 0xa0, 0x3d, 0x9a, 0x7b, 0x23, 0x67, 0x2a, 0xa2, 0x98, 0x4f, 0x03, 0x0a, 0x2d, 0x95, 0x43, 0xae, + 0x98, 0x5a, 0x1c, 0x19, 0xef, 0x42, 0xeb, 0x50, 0x88, 0xd0, 0x14, 0x51, 0xe0, 0x7b, 0x32, 0xac, + 0x52, 0x45, 0x56, 0xe9, 0xfd, 0x15, 0x64, 0xfc, 0x0e, 0xe8, 0x26, 0x3f, 0x8e, 0xb7, 0x78, 0x6c, + 0x9d, 0x7e, 0x97, 0x1a, 0xc1, 0xbb, 0x50, 0x0f, 0xa4, 0x4c, 0xa9, 0x0c, 0xad, 0x45, 0x51, 0x80, + 0x92, 0x33, 0x33, 0x21, 0x1a, 0x1f, 0xc1, 0xcd, 0xe1, 0x6c, 0x12, 0x59, 0xa1, 0x43, 0xd9, 0x6c, + 0xe2, 0x21, 0xfb, 0xd0, 0x08, 0x42, 0x71, 0xec, 0xcc, 0x45, 0xa2, 0x18, 0x29, 0x6c, 0xfc, 0x18, + 0x6e, 0x15, 0x87, 0xa8, 0x23, 0xbc, 0x0d, 0xe5, 0xb3, 0xf3, 0x48, 0xed, 0xec, 0x46, 0x21, 0x39, + 0xa1, 0x67, 0x62, 0xa4, 0x1a, 0x26, 0x94, 0xf7, 0x67, 0xd3, 0xfc, 0xaf, 0x2e, 0x15, 0xf9, 0xab, + 0xcb, 0x9d, 0x7c, 0xcd, 0x53, 0xe6, 0x2f, 0x59, 0x6d, 0xf3, 0x0d, 0xd0, 0x8f, 0xfd, 0xf0, 0x17, + 0x3c, 0xb4, 0x85, 0xad, 0x5c, 0x61, 0x86, 0x30, 0xbe, 0x82, 0x66, 0x22, 0x09, 0xbb, 0x36, 0xbd, + 0x8b, 0x91, 0x28, 0xee, 0xda, 0x05, 0xc9, 0x94, 0x15, 0x45, 0xe1, 0xd9, 0xbb, 0x89, 0x08, 0x49, + 0xa0, 0xb8, 0xb2, 0x7a, 0xce, 0x48, 0x56, 0x36, 0x76, 0xa0, 0x95, 0xa4, 0x7f, 0xcf, 0x44, 0xcc, + 0x49, 0xb8, 0x5d, 0x47, 0x78, 0x39, 0xc1, 0x6f, 0x48, 0xc4, 0xa8, 0x58, 0xf4, 0xd3, 0x0a, 0x71, + 0x85, 0xb1, 0x0e, 0x35, 0xa5, 0x39, 0x0c, 0x2a, 0x96, 0x6f, 0x4b, 0xed, 0xae, 0x9a, 0xd4, 0x46, + 0x76, 0x4c, 0xa3, 0x93, 0x24, 0x66, 0x9a, 0x46, 0x27, 0xc6, 0xdf, 0x69, 0xd0, 0xde, 0xa2, 0x62, + 0x48, 0x72, 0x25, 0xb9, 0xfa, 0x4e, 0xa9, 0x50, 0xdf, 0xc9, 0xd7, 0x72, 0xb4, 0x42, 0x2d, 0xa7, + 0xb0, 0xa1, 0x72, 0x31, 0xd0, 0xf9, 0x3e, 0xd4, 0x67, 0x9e, 0x33, 0x4f, 0x4c, 0x82, 0x6e, 0xd6, + 0x10, 0x1c, 0x45, 0x6c, 0x15, 0x9a, 0x68, 0x35, 0x1c, 0x4f, 0x56, 0x6d, 0x64, 0xe9, 0x25, 0x8f, + 0x5a, 0xa8, 0xcd, 0xd4, 0x5e, 0x5d, 0x9b, 0xa9, 0xbf, 0xb6, 0x36, 0xd3, 0x78, 0x5d, 0x6d, 0x46, + 0x5f, 0xac, 0xcd, 0x14, 0x83, 0x34, 0x58, 0x0c, 0xd2, 0x8c, 0x3f, 0xd7, 0xa0, 0x3d, 0x98, 0x07, + 0xf4, 0xff, 0xc2, 0x6b, 0x23, 0xbe, 0x1c, 0x5f, 0xb5, 0x02, 0x5f, 0x73, 0x1c, 0x2a, 0xab, 0x07, + 0x0b, 0xc9, 0x21, 0x8c, 0x01, 0x65, 0xa5, 0x44, 0x71, 0x4e, 0x42, 0xff, 0x0f, 0x38, 0x67, 0xec, + 0x41, 0x27, 0x61, 0x8c, 0xd2, 0xda, 0x6f, 0x25, 0x8e, 0xf2, 0xdf, 0x23, 0x37, 0xad, 0x1f, 0x48, + 0xc0, 0xf8, 0x63, 0x0d, 0x74, 0x29, 0xa4, 0xb8, 0xbd, 0xf7, 0x55, 0xfc, 0x5a, 0xca, 0xaa, 0xa5, + 0x29, 0x71, 0xfd, 0xa9, 0xb8, 0xa0, 0xb8, 0x4b, 0x86, 0xb5, 0xcb, 0xde, 0x14, 0x94, 0x33, 0x95, + 0x59, 0x17, 0x39, 0xd3, 0x3b, 0xa0, 0x4b, 0x1f, 0x33, 0x73, 0x92, 0x57, 0x48, 0xe9, 0x74, 0x8e, + 0x1c, 0xfa, 0x65, 0x23, 0x16, 0xe1, 0x54, 0x71, 0x99, 0xda, 0xc5, 0xf8, 0xb6, 0xad, 0x22, 0x2e, + 0xe3, 0x14, 0xea, 0x6a, 0x75, 0x0c, 0x40, 0x8e, 0xf6, 0x9f, 0xee, 0x1f, 0x7c, 0xb9, 0xdf, 0xbd, + 0x96, 0xd6, 0x97, 0x4b, 0x59, 0x88, 0xa2, 0xe5, 0x43, 0x94, 0x32, 0xe2, 0x1f, 0x1f, 0x1c, 0xed, + 0x8f, 0xba, 0x15, 0xd6, 0x06, 0x9d, 0x9a, 0x63, 0x73, 0xf0, 0xbc, 0x5b, 0xa5, 0x84, 0xfb, 0xf1, + 0x67, 0x83, 0x67, 0x9b, 0xdd, 0x5a, 0x5a, 0x9d, 0xae, 0x1b, 0x7f, 0x50, 0x82, 0x1b, 0xf2, 0xc8, + 0xf9, 0xf4, 0x34, 0xff, 0xdf, 0x5f, 0x45, 0xfe, 0xf7, 0xf7, 0x6b, 0xce, 0x48, 0xff, 0xa1, 0x04, + 0x7d, 0x19, 0xfc, 0x3c, 0x09, 0x79, 0x70, 0xfa, 0xc5, 0xde, 0xa5, 0xf4, 0xe7, 0x2a, 0xdf, 0x7d, + 0x0f, 0x3a, 0xf4, 0xf3, 0xe3, 0xcf, 0xdd, 0xb1, 0x0a, 0xd1, 0xe5, 0x15, 0xb5, 0x15, 0x56, 0x4e, + 0xc4, 0x3e, 0x86, 0x96, 0xfc, 0x49, 0x92, 0x6a, 0x71, 0x85, 0xe7, 0x8a, 0x42, 0xe8, 0xd5, 0x94, + 0xbd, 0xe8, 0xe1, 0x84, 0x7d, 0x94, 0x0e, 0xca, 0x32, 0xa5, 0xcb, 0x2f, 0x12, 0x6a, 0xc8, 0x88, + 0xf2, 0xa7, 0x87, 0x70, 0x67, 0xe9, 0x39, 0x94, 0xec, 0xe6, 0x0a, 0x53, 0x52, 0x64, 0x36, 0xfe, + 0xbe, 0x04, 0x15, 0xf4, 0x87, 0xec, 0x01, 0xe8, 0x9f, 0x09, 0x1e, 0xc6, 0x13, 0xc1, 0x63, 0x56, + 0xf0, 0x7d, 0x7d, 0x5a, 0x31, 0x7b, 0x15, 0x35, 0xae, 0x3d, 0x2a, 0xb1, 0x75, 0xf9, 0xdf, 0x52, + 0xf2, 0x3b, 0x56, 0x3b, 0xf1, 0xab, 0xe4, 0x77, 0xfb, 0x85, 0xf1, 0xc6, 0xb5, 0x35, 0xea, 0xff, + 0xb9, 0xef, 0x78, 0x8f, 0xe5, 0x6f, 0x36, 0x6c, 0xd1, 0x0f, 0x2f, 0x8e, 0x60, 0x0f, 0xa0, 0xb6, + 0x1b, 0xa1, 0xc3, 0xbf, 0xdc, 0x95, 0xb8, 0x96, 0x8f, 0x05, 0x8c, 0x6b, 0x1b, 0x7f, 0x5d, 0x86, + 0xca, 0x57, 0x22, 0xf4, 0xd9, 0x87, 0x50, 0x57, 0x6f, 0xc8, 0x2c, 0xf7, 0x56, 0xdc, 0xa7, 0x94, + 0x66, 0xe1, 0x71, 0x99, 0x56, 0xe9, 0x4a, 0x76, 0x65, 0x15, 0x54, 0x96, 0x3d, 0x71, 0x5f, 0xda, + 0xd4, 0x27, 0xd0, 0x1d, 0xc6, 0xa1, 0xe0, 0xd3, 0x5c, 0xf7, 0x22, 0xab, 0x96, 0x95, 0x63, 0x89, + 0x5f, 0xf7, 0xa1, 0x26, 0xa3, 0xaa, 0x85, 0x01, 0x8b, 0x95, 0x55, 0xea, 0xfc, 0x1e, 0x34, 0x87, + 0xa7, 0xfe, 0xcc, 0xb5, 0x87, 0x22, 0x3c, 0x17, 0x2c, 0xf7, 0x57, 0x48, 0x3f, 0xd7, 0x36, 0xae, + 0xb1, 0x35, 0x00, 0xe9, 0xc8, 0x8f, 0x50, 0x47, 0xea, 0x48, 0xdb, 0x9f, 0x4d, 0xe5, 0xa4, 0x39, + 0x0f, 0x2f, 0x7b, 0xe6, 0x82, 0xab, 0x57, 0xf5, 0xfc, 0x18, 0xda, 0x8f, 0x49, 0x5f, 0x0e, 0xc2, + 0xcd, 0x89, 0x1f, 0xc6, 0x6c, 0xf1, 0xcf, 0x90, 0xfe, 0x22, 0xc2, 0xb8, 0xc6, 0x1e, 0x41, 0x63, + 0x14, 0x5e, 0xc8, 0xfe, 0x37, 0x54, 0x4c, 0x9a, 0xad, 0xb7, 0xe4, 0x94, 0x1b, 0xff, 0x55, 0x81, + 0xda, 0x97, 0x7e, 0x78, 0x26, 0x42, 0x4c, 0x6f, 0xa9, 0x12, 0xae, 0xc4, 0x28, 0xad, 0x8a, 0x2f, + 0x5b, 0xe8, 0x1d, 0xd0, 0x89, 0x29, 0x23, 0x1e, 0x9d, 0xc9, 0xab, 0xa2, 0xbf, 0x6d, 0x25, 0x5f, + 0x64, 0xba, 0x4c, 0xf7, 0xda, 0x91, 0x17, 0x95, 0xbe, 0x16, 0x15, 0xea, 0xd2, 0x7d, 0x3a, 0xff, + 0xd3, 0xe7, 0x43, 0x14, 0xcd, 0x47, 0x25, 0x34, 0xc4, 0x43, 0x79, 0x52, 0xec, 0x94, 0xfd, 0x65, + 0x28, 0x25, 0x3f, 0xfb, 0xad, 0xcf, 0xb8, 0xc6, 0x1e, 0x42, 0x4d, 0xa9, 0xf4, 0x8d, 0x4c, 0x79, + 0x95, 0x9d, 0xe8, 0x77, 0xf3, 0x28, 0x35, 0xe0, 0x7d, 0xa8, 0x49, 0x0b, 0x27, 0x07, 0x14, 0x42, + 0x14, 0xb9, 0x6b, 0x19, 0xe6, 0x18, 0xd7, 0xd8, 0x7d, 0xa8, 0xab, 0x6a, 0x36, 0x5b, 0x52, 0xda, + 0x5e, 0xe8, 0xfc, 0x11, 0xd4, 0xa4, 0x63, 0x92, 0xf3, 0x16, 0xbc, 0x77, 0x9f, 0xe5, 0x51, 0x89, + 0x92, 0xa0, 0xb4, 0x9b, 0xc2, 0x12, 0x4e, 0x2e, 0x8d, 0x62, 0x09, 0x27, 0x96, 0xa8, 0xec, 0x27, + 0xd0, 0x2e, 0xa4, 0x5c, 0xac, 0x47, 0xb7, 0xb3, 0x24, 0x0b, 0xbb, 0xa4, 0x28, 0x3f, 0x06, 0x5d, + 0x45, 0xbc, 0x13, 0xc1, 0xa8, 0x3e, 0xbd, 0x24, 0x66, 0xee, 0x5f, 0x0e, 0x79, 0x49, 0xfa, 0x7f, + 0x02, 0x37, 0x97, 0xd8, 0x30, 0x46, 0xbf, 0xe2, 0x5c, 0x6d, 0xa4, 0xfb, 0x2b, 0x57, 0xd2, 0x13, + 0x06, 0x6c, 0x75, 0xff, 0xf1, 0x9b, 0xbb, 0xa5, 0x7f, 0xf9, 0xe6, 0x6e, 0xe9, 0xdf, 0xbe, 0xb9, + 0x5b, 0xfa, 0xe5, 0xbf, 0xdf, 0xbd, 0x36, 0xa9, 0xd1, 0x1f, 0xe7, 0x1f, 0xff, 0x5f, 0x00, 0x00, + 0x00, 0xff, 0xff, 0x50, 0xfd, 0x11, 0x9e, 0xe7, 0x2e, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -7964,6 +7973,15 @@ func (m *DirectedEdge) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if len(m.AllowedPreds) > 0 { + for iNdEx := len(m.AllowedPreds) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.AllowedPreds[iNdEx]) + copy(dAtA[i:], m.AllowedPreds[iNdEx]) + i = encodeVarintPb(dAtA, i, uint64(len(m.AllowedPreds[iNdEx]))) + i-- + dAtA[i] = 0x52 + } + } if len(m.Facets) > 0 { for iNdEx := len(m.Facets) - 1; iNdEx >= 0; iNdEx-- { { @@ -11290,6 +11308,12 @@ func (m *DirectedEdge) Size() (n int) { n += 1 + l + sovPb(uint64(l)) } } + if len(m.AllowedPreds) > 0 { + for _, s := range m.AllowedPreds { + l = len(s) + n += 1 + l + sovPb(uint64(l)) + } + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -16771,6 +16795,38 @@ func (m *DirectedEdge) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 10: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AllowedPreds", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowPb + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthPb + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthPb + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AllowedPreds = append(m.AllowedPreds, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipPb(dAtA[iNdEx:]) diff --git a/query/mutation.go b/query/mutation.go index b21efb6de78..d110a646304 100644 --- a/query/mutation.go +++ b/query/mutation.go @@ -70,6 +70,22 @@ func expandEdges(ctx context.Context, m *pb.Mutations) ([]*pb.DirectedEdge, erro } preds = append(preds, getPredicatesFromTypes(types)...) preds = append(preds, x.StarAllPredicates()...) + // AllowedPreds are used only with ACL. Do not delete all predicates but + // delete predicates to which the mutation has access + if edge.AllowedPreds != nil { + // Take intersection of preds and AllowedPreds + intersectPreds := make([]string, 0) + hashMap := make(map[string]bool) + for _, allowedPred := range edge.AllowedPreds { + hashMap[allowedPred] = true + } + for _, pred := range preds { + if _, found := hashMap[pred]; found { + intersectPreds = append(intersectPreds, pred) + } + } + preds = intersectPreds + } } for _, pred := range preds { @@ -200,6 +216,11 @@ func ToDirectedEdges(gmuList []*gql.Mutation, newUids map[string]uint64) ( if err := parse(nq, pb.DirectedEdge_DEL); err != nil { return edges, err } + if gmu.AllowedPreds != nil { + for _, e := range edges { + e.AllowedPreds = gmu.AllowedPreds + } + } } for _, nq := range gmu.Set { if err := facets.SortAndValidate(nq.Facets); err != nil { diff --git a/raftwal/storage.go b/raftwal/storage.go index a3ad8bc4b1b..be3e399869e 100644 --- a/raftwal/storage.go +++ b/raftwal/storage.go @@ -23,9 +23,9 @@ import ( "sync" "github.com/dgraph-io/badger/v2" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" "github.com/gogo/protobuf/proto" "github.com/golang/glog" "github.com/pkg/errors" @@ -42,7 +42,7 @@ type DiskStorage struct { elog trace.EventLog cache *sync.Map - Closer *y.Closer + Closer *z.Closer indexRangeChan chan indexRange } @@ -57,7 +57,7 @@ func Init(db *badger.DB, id uint64, gid uint32) *DiskStorage { id: id, gid: gid, cache: new(sync.Map), - Closer: y.NewCloser(1), + Closer: z.NewCloser(1), indexRangeChan: make(chan indexRange, 16), } if prev, err := RaftId(db); err != nil || prev != id { diff --git a/systest/group-delete/group_delete_test.go b/systest/group-delete/group_delete_test.go index 7b7653cf970..de0ce3dc96f 100644 --- a/systest/group-delete/group_delete_test.go +++ b/systest/group-delete/group_delete_test.go @@ -125,7 +125,15 @@ func getError(rc io.ReadCloser) error { } func TestNodes(t *testing.T) { - dg, err := testutil.GetClientToGroup("1") + var dg *dgo.Dgraph + var err error + for i := 0; i < 3; i++ { + dg, err = testutil.GetClientToGroup("1") + if err == nil { + break + } + time.Sleep(5*time.Second) + } require.NoError(t, err, "error while getting connection to group 1") NodesSetup(t, dg) diff --git a/systest/mutations_test.go b/systest/mutations_test.go index cfe7be2b706..f7d6544440d 100644 --- a/systest/mutations_test.go +++ b/systest/mutations_test.go @@ -100,6 +100,7 @@ func TestSystem(t *testing.T) { t.Run("count index delete on non list predicate", wrap(CountIndexNonlistPredicateDelete)) t.Run("Reverse count index delete", wrap(ReverseCountIndexDelete)) t.Run("overwrite uid predicates", wrap(OverwriteUidPredicates)) + t.Run("overwrite uid predicates across txns", wrap(OverwriteUidPredicatesMultipleTxn)) t.Run("overwrite uid predicates reverse index", wrap(OverwriteUidPredicatesReverse)) t.Run("delete and query same txn", wrap(DeleteAndQuerySameTxn)) } @@ -2341,6 +2342,60 @@ func OverwriteUidPredicatesReverse(t *testing.T, c *dgo.Dgraph) { } +func OverwriteUidPredicatesMultipleTxn(t *testing.T, c *dgo.Dgraph) { + ctx := context.Background() + op := &api.Operation{DropAll: true} + require.NoError(t, c.Alter(ctx, op)) + + op = &api.Operation{ + Schema: ` + best_friend: uid . + name: string @index(exact) .`, + } + err := c.Alter(ctx, op) + require.NoError(t, err) + + resp, err := c.NewTxn().Mutate(context.Background(), &api.Mutation{ + CommitNow: true, + SetNquads: []byte(` + _:alice "Alice" . + _:bob "Bob" . + _:alice _:bob .`), + }) + require.NoError(t, err) + + alice := resp.Uids["alice"] + bob := resp.Uids["bob"] + + txn := c.NewTxn() + _, err = txn.Mutate(context.Background(), &api.Mutation{ + DelNquads: []byte(fmt.Sprintf("<%s> <%s> .", alice, bob)), + }) + require.NoError(t, err) + + _, err = txn.Mutate(context.Background(), &api.Mutation{ + SetNquads: []byte(fmt.Sprintf(`<%s> _:carl . + _:carl "Carl" .`, alice)), + }) + require.NoError(t, err) + err = txn.Commit(context.Background()) + require.NoError(t, err) + + query := fmt.Sprintf(`{ + me(func:uid(%s)) { + name + best_friend { + name + } + } + }`, alice) + + resp, err = c.NewReadOnlyTxn().Query(ctx, query) + require.NoError(t, err) + testutil.CompareJSON(t, `{"me":[{"name":"Alice","best_friend": {"name": "Carl"}}]}`, + string(resp.GetJson())) +} + func DeleteAndQuerySameTxn(t *testing.T, c *dgo.Dgraph) { // Set the schema. ctx := context.Background() diff --git a/testutil/graphql.go b/testutil/graphql.go index 009f9c81b45..e3457003d87 100644 --- a/testutil/graphql.go +++ b/testutil/graphql.go @@ -145,11 +145,12 @@ func (c clientCustomClaims) MarshalJSON() ([]byte, error) { } type AuthMeta struct { - PublicKey string - Namespace string - Algo string - Header string - AuthVars map[string]interface{} + PublicKey string + Namespace string + Algo string + Header string + AuthVars map[string]interface{} + PrivateKeyPath string } func (a *AuthMeta) GetSignedToken(privateKeyFile string, diff --git a/wiki/config.toml b/wiki/config.toml index 275d2255766..092663eb784 100644 --- a/wiki/config.toml +++ b/wiki/config.toml @@ -1,17 +1,28 @@ +canonifyURLs = true languageCode = "en-us" theme = "hugo-docs" -canonifyURLs = true [markup.goldmark.renderer] unsafe = true [markup.highlight] -noClasses = false +codeFences = true +guessSyntax = false +hl_Lines = "" +lineNoStart = 1 +lineNos = false +lineNumbersInTable = true +noClasses = true +style = "vs" +tabWidth = 4 [markup.tableOfContents] - endLevel = 3 - ordered = false - startLevel = 2 +endLevel = 3 +ordered = false +startLevel = 2 # set by build script: title, baseurl -title = "Dgraph Documentation" \ No newline at end of file +title = "Dgraph Documentation" + +[params] +discourse = "https://discuss.dgraph.io/" diff --git a/wiki/content/_index.md b/wiki/content/_index.md index 14638e022a1..a519e53500a 100644 --- a/wiki/content/_index.md +++ b/wiki/content/_index.md @@ -1,6 +1,7 @@ +++ date = "2017-03-20T19:35:35+11:00" title = "Dgraph Documentation" +aliases = ["/contribute"] [menu.main] url = "/" name = "Home" @@ -10,13 +11,37 @@ title = "Dgraph Documentation" **Welcome to the official Dgraph documentation.** -Dgraph is an open-source, scalable, distributed, highly available and fast graph database, designed from the ground up to be run in production. +Designed from the ground up to be run in production, Dgraph is the native GraphQL database with a graph backend. It is open-source, scalable, distributed, highly available and lightning fast. ## Using Dgraph
+
+
+ +

+ Get Started with GraphQL +

+
+
+
+
+ +

+ Slash GraphQL Provides /graphql Backend for Your App +

+
+
@@ -125,6 +150,18 @@ Dgraph is an open-source, scalable, distributed, highly available and fast graph

+
+
+ +

+ Embeddable, persistent and fast key-value database that powers Dgraph +

+
+
@@ -157,18 +194,6 @@ Dgraph is an open-source, scalable, distributed, highly available and fast graph
-
-
- -

- Chat instantly to the Dgraph community and engineers. -

-
-
diff --git a/wiki/content/clients/raw-http.md b/wiki/content/clients/raw-http.md index 3bd38f09992..a7ea967cf97 100644 --- a/wiki/content/clients/raw-http.md +++ b/wiki/content/clients/raw-http.md @@ -316,7 +316,7 @@ The result: ## Running read-only queries You can set the query parameter `ro=true` to `/query` to set it as a -[read-only]({{< relref "#read-only-transactions" >}}) query. +[read-only]({{< relref "clients/go.md#read-only-transactions" >}}) query. ```sh @@ -333,7 +333,7 @@ $ curl -H "Content-Type: application/graphql+-" -X POST "localhost:8080/query?ro ## Running best-effort queries You can set the query parameter `be=true` to `/query` to set it as a -[best-effort]({{< relref "#read-only-transactions" >}}) query. +[best-effort]({{< relref "clients/go.md#read-only-transactions" >}}) query. ```sh diff --git a/wiki/content/deploy/cluster-checklist.md b/wiki/content/deploy/cluster-checklist.md index 44e3a18595d..78ed90487da 100644 --- a/wiki/content/deploy/cluster-checklist.md +++ b/wiki/content/deploy/cluster-checklist.md @@ -14,4 +14,4 @@ In setting up a cluster be sure the check the following. * Does each instance have a unique ID on startup? * Has `--bindall=true` been set for networked communication? -See the [Production Checklist]({{< relref "#production-checklist" >}}) docs for more info. \ No newline at end of file +See the [Production Checklist]({{< relref "deploy/production-checklist.md" >}}) docs for more info. diff --git a/wiki/content/deploy/dgraph-administration.md b/wiki/content/deploy/dgraph-administration.md index 6fbae894f40..4760f982e0e 100644 --- a/wiki/content/deploy/dgraph-administration.md +++ b/wiki/content/deploy/dgraph-administration.md @@ -156,7 +156,7 @@ This stops the Alpha on which the command is executed and not the entire cluster ## Deleting database -Individual triples, patterns of triples and predicates can be deleted as described in the [query languge docs](/query-language#delete). +Individual triples, patterns of triples and predicates can be deleted as described in the [DQL docs]({{< relref "mutations/delete.md" >}}). To drop all data, you could send a `DropAll` request via `/alter` endpoint. @@ -174,7 +174,7 @@ Doing periodic exports is always a good idea. This is particularly useful if you 2. Ensure it is successful 3. [Shutdown Dgraph]({{< relref "#shutting-down-database" >}}) and wait for all writes to complete 4. Start a new Dgraph cluster using new data directories (this can be done by passing empty directories to the options `-p` and `-w` for Alphas and `-w` for Zeros) -5. Reload the data via [bulk loader]({{< relref "#bulk-loader" >}}) +5. Reload the data via [bulk loader]({{< relref "deploy/fast-data-loading.md#bulk-loader" >}}) 6. Verify the correctness of the new Dgraph cluster. If all looks good, you can delete the old directories (export serves as an insurance) These steps are necessary because Dgraph's underlying data format could have changed, and reloading the export avoids encoding incompatibilities. @@ -225,7 +225,7 @@ new type name and copy data from old predicate name to new predicate name for al are affected. Then, you can drop the old types and predicates from DB. {{% notice "note" %}} -If you are upgrading from v1.0, please make sure you follow the schema migration steps described in [this section](/howto/#schema-types-scalar-uid-and-list-uid). +If you are upgrading from v1.0, please make sure you follow the schema migration steps described in [this section]({{< relref "howto/migrate-dgraph-1-1.md" >}}). {{% /notice %}} ## Post Installation diff --git a/wiki/content/deploy/dgraph-alpha.md b/wiki/content/deploy/dgraph-alpha.md index b3e126a4343..a8d8d9beb46 100644 --- a/wiki/content/deploy/dgraph-alpha.md +++ b/wiki/content/deploy/dgraph-alpha.md @@ -12,8 +12,8 @@ These HTTP endpoints are deprecated and will be removed in the next release. Ple {{% /notice %}} * `/health?all` returns information about the health of all the servers in the cluster. -* `/admin/shutdown` initiates a proper [shutdown]({{< relref "#shutdown" >}}) of the Alpha. -* `/admin/export` initiates a data [export]({{< relref "#export" >}}). The exported data will be +* `/admin/shutdown` initiates a proper [shutdown]({{< relref "deploy/dgraph-administration.md#shutting-down-database" >}}) of the Alpha. +* `/admin/export` initiates a data [export]({{< relref "deploy/dgraph-administration.md#exporting-database" >}}). The exported data will be encrypted if the alpha instance was configured with an encryption key file. By default the Alpha listens on `localhost` for admin actions (the loopback address only accessible from the same machine). The `--bindall=true` option binds to `0.0.0.0` and thus allows external connections. @@ -81,4 +81,4 @@ Here’s an example of JSON returned from the above query: - `ongoing`: List of ongoing operations in the background. - `indexing`: List of predicates for which indexes are built in the background. Read more [here]({{< relref "/query-language/schema.md#indexes-in-background" >}}). -The same information (except `ongoing` and `indexing`) is available from the `/health` and `/health?all` endpoints of Alpha server. \ No newline at end of file +The same information (except `ongoing` and `indexing`) is available from the `/health` and `/health?all` endpoints of Alpha server. diff --git a/wiki/content/deploy/download.md b/wiki/content/deploy/download.md index 62e792d0de0..11a8b4f1640 100644 --- a/wiki/content/deploy/download.md +++ b/wiki/content/deploy/download.md @@ -60,7 +60,7 @@ curl https://get.dgraph.io -sSf | VERSION=v2.0.0-beta1 bash ``` {{% notice "note" %}} -Be aware that using this script will overwrite the installed version and can lead to compatibility problems. For example, if you were using version v1.0.5 and forced the installation of v2.0.0-Beta, the existing data won't be compatible with the new version. The data must be [exported]({{< relref "deploy/index.md#exporting-database" >}}) before running this script and reimported to the new cluster running the updated version. +Be aware that using this script will overwrite the installed version and can lead to compatibility problems. For example, if you were using version v1.0.5 and forced the installation of v2.0.0-Beta, the existing data won't be compatible with the new version. The data must be [exported]({{< relref "deploy/dgraph-administration.md#exporting-database" >}}) before running this script and reimported to the new cluster running the updated version. {{% /notice %}} ## Manual download [optional] diff --git a/wiki/content/deploy/fast-data-loading.md b/wiki/content/deploy/fast-data-loading.md index bd4b269e887..528d064200c 100644 --- a/wiki/content/deploy/fast-data-loading.md +++ b/wiki/content/deploy/fast-data-loading.md @@ -270,7 +270,8 @@ ending in .rdf, .rdf.gz, .json, and .json.gz will be loaded. `--format`: Specify file format (rdf or json) instead of getting it from filenames. This is useful if you need to define a strict format manually. -`--store_xids`: Generate a xid edge for each node. It will store the XIDs (The identifier / Blank-nodes) in an attribute named `xid` in the entity itself. It is useful if you gonna use [External IDs](/mutations#external-ids). +`--store_xids`: Generate a xid edge for each node. It will store the XIDs (The identifier / Blank-nodes) in an attribute named `xid` in the entity itself. It is useful if you gonna +use [External IDs]({{< relref "mutations/external-ids.md" >}}). `--xidmap` (default: disabled. Need a path): Store xid to uid mapping to a directory. Dgraph will save all identifiers used in the load for later use in other data ingest operations. The mapping will be saved in the path you provide and you must indicate that same path in the next load. It is recommended to use this flag if you have full control over your identifiers (Blank-nodes). Because the identifier will be mapped to a specific UID. @@ -320,4 +321,4 @@ higher CPU utilization. Dgraph alpha instances more evenly. - The `--shufflers` controls the level of parallelism in the shuffle/reduce - stage. Increasing this increases memory consumption. \ No newline at end of file + stage. Increasing this increases memory consumption. diff --git a/wiki/content/deploy/index.md b/wiki/content/deploy/index.md deleted file mode 100644 index 7c86f02dcdb..00000000000 --- a/wiki/content/deploy/index.md +++ /dev/null @@ -1,4 +0,0 @@ -+++ -date = "2017-03-20T22:25:17+11:00" -title = "Deploy" -+++ diff --git a/wiki/content/deploy/kubernetes.md b/wiki/content/deploy/kubernetes.md index 5005244b594..b6610106d75 100644 --- a/wiki/content/deploy/kubernetes.md +++ b/wiki/content/deploy/kubernetes.md @@ -8,8 +8,8 @@ title = "Using Kubernetes" The following section covers running Dgraph with Kubernetes. We have tested Dgraph with Kubernetes 1.14 to 1.15 on [GKE](https://cloud.google.com/kubernetes-engine) and [EKS](https://aws.amazon.com/eks/). -{{% notice "note" %}}These instructions are for running Dgraph Alpha without TLS configuration. -Instructions for running with TLS refer [TLS instructions](#tls-configuration).{{% /notice %}} +{{% notice "note" %}}These instructions are for running Dgraph alpha service without TLS configuration. +Instructions for running Dgraph alpha service with TLS refer [TLS instructions]({{< relref "deploy/tls-configuration.md" >}}).{{% /notice %}} * Install [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) which is used to deploy and manage applications on kubernetes. @@ -257,7 +257,7 @@ upgrade the configuration in multiple steps following the steps below. #### Upgrade to HA cluster setup -To upgrade to an [HA cluster setup]({{< relref "#ha-cluster-setup" >}}), ensure +To upgrade to an [HA cluster setup]({{< relref "#ha-cluster-setup-using-kubernetes" >}}), ensure that the shard replication setting is more than 1. When `zero.shardReplicaCount` is not set to an HA configuration (3 or 5), follow the steps below: @@ -557,10 +557,10 @@ configuration to be updated. ## Kubernetes and Bulk Loader You may want to initialize a new cluster with an existing data set such as data -from the [Dgraph Bulk Loader]({{< relref "#bulk-loader" >}}). You can use [Init +from the [Dgraph Bulk Loader]({{< relref "deploy/fast-data-loading.md#bulk-loader" >}}). You can use [Init Containers](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/) to copy the data to the pod volume before the Alpha process runs. See the `initContainers` configuration in [dgraph-ha.yaml](https://github.com/dgraph-io/dgraph/blob/master/contrib/config/kubernetes/dgraph-ha/dgraph-ha.yaml) -to learn more. \ No newline at end of file +to learn more. diff --git a/wiki/content/deploy/multi-host-setup.md b/wiki/content/deploy/multi-host-setup.md index aa000546f28..e380a0507c6 100644 --- a/wiki/content/deploy/multi-host-setup.md +++ b/wiki/content/deploy/multi-host-setup.md @@ -11,7 +11,7 @@ title = "Multi Host Setup" ### Cluster Setup Using Docker Swarm {{% notice "note" %}}These instructions are for running Dgraph Alpha without TLS config. -Instructions for running with TLS refer [TLS instructions](#tls-configuration).{{% /notice %}} +Instructions for running with TLS refer [TLS instructions]({{< relref "deploy/tls-configuration.md" >}}).{{% /notice %}} Here we'll go through an example of deploying 3 Dgraph Alpha nodes and 1 Zero on three different AWS instances using Docker Swarm with a replication factor of 3. @@ -261,4 +261,4 @@ docker stack rm dgraph {{% notice "note" %}} 1. This setup assumes that you are using 6 hosts, but if you are running fewer than 6 hosts then you have to either use different volumes between Dgraph alphas or use `-p` & `-w` to configure data directories. -2. This setup would create and use a local volume called `dgraph_data-volume` on the instances. If you plan to replace instances, you should use remote storage like [cloudstore](https://docs.docker.com/docker-for-aws/persistent-data-volumes) instead of local disk. {{% /notice %}} \ No newline at end of file +2. This setup would create and use a local volume called `dgraph_data-volume` on the instances. If you plan to replace instances, you should use remote storage like [cloudstore](https://docs.docker.com/docker-for-aws/persistent-data-volumes) instead of local disk. {{% /notice %}} diff --git a/wiki/content/deploy/ports-usage.md b/wiki/content/deploy/ports-usage.md index f701ed75826..e3ff2539f05 100644 --- a/wiki/content/deploy/ports-usage.md +++ b/wiki/content/deploy/ports-usage.md @@ -23,9 +23,9 @@ Dgraph cluster nodes use different ports to communicate over gRPC and HTTP. User ratel | --Not Used-- | --Not Used-- | 8000 -1: Dgraph Zero's gRPC-internal port is used for internal communication within the cluster. It's also needed for the [fast data loading]({{< relref "#fast-data-loading" >}}) tools Dgraph Live Loader and Dgraph Bulk Loader. +1: Dgraph Zero's gRPC-internal port is used for internal communication within the cluster. It's also needed for the [fast data loading]({{< relref "deploy/fast-data-loading.md" >}}) tools Dgraph Live Loader and Dgraph Bulk Loader. -2: Dgraph Zero's HTTP-external port is used for [admin]({{< relref "#more-about-dgraph-zero" >}}) operations. Access to it is not required by clients. +2: Dgraph Zero's HTTP-external port is used for [admin]({{< relref "deploy/dgraph-zero.md" >}}) operations. Access to it is not required by clients. Users have to modify security rules or open firewall ports depending up on their underlying network to allow communication between cluster nodes and between the Dgraph instances themselves and between Dgraph and a client. A general rule is to make *-external (gRPC/HTTP) ports wide open to clients and gRPC-internal ports open within the cluster nodes. @@ -74,4 +74,4 @@ Over time, the data would be evenly split across all the groups. So, it's important to ensure that the number of Dgraph alphas is a multiple of the replication setting. For e.g., if you set `--replicas=3` in Zero, then run three Dgraph alphas for no sharding, but 3x replication. Run six Dgraph alphas, for -sharding the data into two groups, with 3x replication. \ No newline at end of file +sharding the data into two groups, with 3x replication. diff --git a/wiki/content/deploy/production-checklist.md b/wiki/content/deploy/production-checklist.md index 3844d31f62a..92a623baae3 100644 --- a/wiki/content/deploy/production-checklist.md +++ b/wiki/content/deploy/production-checklist.md @@ -107,7 +107,7 @@ We provide sample configs for both [Docker Compose](https://github.com/dgraph-io {{% load-img "/images/deploy-guide-1.png" "2-node cluster" %}} -Configuration can be set either as command-line flags, environment variables, or in a config file (see [Config]({{< relref "deploy/_index.md#config" >}})). +Configuration can be set either as command-line flags, environment variables, or in a config file (see [Config]({{< relref "deploy/config.md" >}})). Dgraph Zero: * The `--my` flag should be set to the address:port (the internal-gRPC port) that will be accessible to the Dgraph Alpha (default: `localhost:5080`). @@ -130,7 +130,7 @@ We provide sample configs for both [Docker Compose](https://github.com/dgraph-io A Dgraph cluster can be configured in a high-availability setup with Dgraph Zero and Dgraph Alpha each set up with peers. These peers are part of Raft consensus groups, which elect a single leader amongst themselves. The non-leader peers are called followers. In the event that the peers cannot communicate with the leader (e.g., a network partition or a machine shuts down), the group automatically elects a new leader to continue. -Configuration can be set either as command-line flags, environment variables, or in a config file (see [Config]({{< relref "deploy/_index.md#config" >}})). +Configuration can be set either as command-line flags, environment variables, or in a config file (see [Config]({{< relref "deploy/config.md" >}})). In this setup, we assume the following hostnames are set: diff --git a/wiki/content/deploy/single-host-setup.md b/wiki/content/deploy/single-host-setup.md index b8b096f47a2..7acd3c77f22 100644 --- a/wiki/content/deploy/single-host-setup.md +++ b/wiki/content/deploy/single-host-setup.md @@ -85,7 +85,7 @@ We will use [Docker Machine](https://docs.docker.com/machine/overview/). It is a * [Install Docker Machine](https://docs.docker.com/machine/install-machine/) on your machine. {{% notice "note" %}}These instructions are for running Dgraph Alpha without TLS config. -Instructions for running with TLS refer [TLS instructions](#tls-configuration).{{% /notice %}} +Instructions for running with TLS refer [TLS instructions]({{< relref "deploy/tls-configuration.md" >}}).{{% /notice %}} Here we'll go through an example of deploying Dgraph Zero, Alpha and Ratel on an AWS instance. @@ -145,4 +145,4 @@ Finally run the command below to start the Zero and Alpha. docker-compose up -d ``` This would start 3 Docker containers running Dgraph Zero, Alpha and Ratel on the same machine. Docker would restart the containers in case there is any error. -You can look at the logs using `docker-compose logs`. \ No newline at end of file +You can look at the logs using `docker-compose logs`. diff --git a/wiki/content/deploy/troubleshooting.md b/wiki/content/deploy/troubleshooting.md index 28440375407..24e18be5321 100644 --- a/wiki/content/deploy/troubleshooting.md +++ b/wiki/content/deploy/troubleshooting.md @@ -20,8 +20,16 @@ You could also decrease memory usage of Dgraph by setting `--badger.vlog=disk`. ### Too many open files -If you see an log error messages saying `too many open files`, you should increase the per-process file descriptors limit. +If you see a log error messages saying `too many open files`, you should increase the per-process file descriptors limit. During normal operations, Dgraph must be able to open many files. Your operating system may set by default a open file descriptor limit lower than what's needed for a database such as Dgraph. -On Linux and Mac, you can check the file descriptor limit with `ulimit -n -H` for the hard limit and `ulimit -n -S` for the soft limit. The soft limit should be set high enough for Dgraph to run properly. A soft limit of 65535 is a good lower bound for a production setup. You can adjust the limit as needed. \ No newline at end of file +On Linux and Mac, you can check the file descriptor limit with `ulimit -n -H` for the hard limit and `ulimit -n -S` for the soft limit. The soft limit should be set high enough for Dgraph to run properly. A soft limit of 65535 is a good lower bound for a production setup. You can adjust the limit as needed. + +### Unauthorized IP address: X.X.X.X + +To ensure security around admin operations, the default behaviour of admin operations has been changed. + +Now by default, all the admin operations will be restricted from the alpha machine itself. Any admin operations outside the alpha environment will be rejected with unauthorized access error. + +You can use the `--whitelist` option to specify whitelisted IP addresses or ranges for hosts from which admin operations can be initiated. More about whitelisting IPs can be found [here]({{< relref "dgraph-administration.md" >}}). \ No newline at end of file diff --git a/wiki/content/design-concepts/concepts.md b/wiki/content/design-concepts/concepts.md index e287f62d91d..afbcc9e4e84 100644 --- a/wiki/content/design-concepts/concepts.md +++ b/wiki/content/design-concepts/concepts.md @@ -17,7 +17,7 @@ Both the terminologies get used interchangeably in our code. Dgraph considers ed i.e. from `Subject -> Object`. This is the direction that the queries would be run. {{% notice "tip" %}}Dgraph can automatically generate a reverse edge. If the user wants to run -queries in that direction, they would need to define the [reverse edge](/query-language#reverse-edges) +queries in that direction, they would need to define the [reverse edge]({{< relref "query-language/schema.md#reverse-edges" >}}) as part of the schema.{{% /notice %}} Internally, the RDF N-Quad gets parsed into this format. @@ -157,7 +157,7 @@ Each group should typically be served by at least 3 servers, if available. In th failure, other servers serving the same group can still handle the load in that case. ## New Server and Discovery -Dgraph cluster can detect new machines allocated to the [cluster](/deploy#cluster), +Dgraph cluster can detect new machines allocated to the [cluster]({{< relref "deploy/cluster-setup.md" >}}), establish connections, and transfer a subset of existing predicates to it based on the groups served by the new machine. @@ -256,4 +256,4 @@ This avoids having to recreate a new connection every time a network call needs All data in Dgraph that is stored or transmitted is first converted into byte arrays through serialization using [Protocol Buffers](https://developers.google.com/protocol-buffers/). When the result is to be returned to the user, the protocol buffer object is traversed, and the JSON -object is formed. \ No newline at end of file +object is formed. diff --git a/wiki/content/dgraph-compared-to-other-databases/index.md b/wiki/content/dgraph-compared-to-other-databases/index.md index eb3c063ac59..47a078ee343 100644 --- a/wiki/content/dgraph-compared-to-other-databases/index.md +++ b/wiki/content/dgraph-compared-to-other-databases/index.md @@ -26,7 +26,7 @@ Graph databases optimize internal data representation to be able to do graph ope ### Language -Dgraph supports [GraphQL+-]({{< relref "query-language/_index.md#graphql">}}), +Dgraph supports [GraphQL+-]({{< relref "query-language/graphql-fundamentals.md">}}), a variation of [GraphQL](https://graphql.org/), a query language created by Facebook. GraphQL+-, as GraphQL itself, allows results to be produced as subgraph rather than lists. diff --git a/wiki/content/dql/_index.md b/wiki/content/dql/_index.md new file mode 100644 index 00000000000..50d0160cc03 --- /dev/null +++ b/wiki/content/dql/_index.md @@ -0,0 +1,7 @@ ++++ +title = "DQL" +[menu.main] + url = "/dql/" + identifier = "dql" + weight = 4 ++++ \ No newline at end of file diff --git a/wiki/content/enterprise-features/binary-backups.md b/wiki/content/enterprise-features/binary-backups.md index 9a3c2b9a572..11259c70d1d 100644 --- a/wiki/content/enterprise-features/binary-backups.md +++ b/wiki/content/enterprise-features/binary-backups.md @@ -55,7 +55,6 @@ endpoints, this is only accessible on the same machine as the Alpha unless [whitelisted for admin operations]({{< relref "deploy/dgraph-administration.md#whitelisting-admin-operations" >}}). Execute the following mutation on /admin endpoint using any GraphQL compatible client like Insomnia, GraphQL Playground or GraphiQL. - ### Backup to Amazon S3 ```graphql @@ -210,6 +209,22 @@ mutation { } ``` +### Automating Backups + +You can use the provided endpoint to automate backups, however, there are a few +things to keep in mind. + +- The requests should go to a single alpha. The alpha that receives the request +is responsible for looking up the location and determining from which point the +backup should resume. + +- Versions of Dgraph starting with v20.07.1, v20.03.5, and v1.2.7 have a way to +block multiple backup requests going to the same alpha. For previous versions, +keep this in mind and avoid sending multiple requests at once. This is for the +same reason as the point above. + +- You can have multiple backup series in the same location although the feature +still works if you set up a unique location for each series. ## Encrypted Backups @@ -316,4 +331,4 @@ $ dgraph restore -p /var/db/dgraph -l /var/backups/dgraph Specify the Zero address and port for the new cluster with `--zero`/`-z` to update the timestamp. ```sh $ dgraph restore -p /var/db/dgraph -l /var/backups/dgraph -z localhost:5080 -``` \ No newline at end of file +``` diff --git a/wiki/content/enterprise-features/encryption-at-rest.md b/wiki/content/enterprise-features/encryption-at-rest.md index 479849f0ac8..faabd4fdd52 100644 --- a/wiki/content/enterprise-features/encryption-at-rest.md +++ b/wiki/content/enterprise-features/encryption-at-rest.md @@ -9,7 +9,7 @@ title = "Encryption at Rest" {{% notice "note" %}} This feature was introduced in [v1.1.1](https://github.com/dgraph-io/dgraph/releases/tag/v1.1.1). For migrating unencrypted data to a new Dgraph cluster with encryption enabled, you need to -[export the database](https://dgraph.io/docs/deploy/#exporting-database) and [fast data load](https://dgraph.io/docs/deploy/#fast-data-loading), +[export the database](https://dgraph.io/docs/deploy/dgraph-administration/#exporting-database) and [fast data load](https://dgraph.io/docs/deploy/#fast-data-loading), preferably using the [bulk loader](https://dgraph.io/docs/deploy/#bulk-loader). {{% /notice %}} @@ -101,4 +101,3 @@ badger rotate --dir w --old-key-path enc_key_file --new-key-path new_enc_key_fil ``` Then, you can start Alpha with the `new_enc_key_file` key file to use the new key. - diff --git a/wiki/content/faq/index.md b/wiki/content/faq/index.md index 221649a33f5..2d65c9b2fba 100644 --- a/wiki/content/faq/index.md +++ b/wiki/content/faq/index.md @@ -24,7 +24,7 @@ If you're running more than five tables in a traditional relational database man If your data doesn't have graph structure, i.e., there's only one predicate, then any graph database might not be a good fit for you. A NoSQL datastore is best for key-value type storage. ### Is Dgraph production ready? -We recommend Dgraph to be used in production at companies. Minor releases at this stage might not be backward compatible; so we highly recommend using [frequent exports](/deploy#exporting-database). +We recommend Dgraph to be used in production at companies. Minor releases at this stage might not be backward compatible; so we highly recommend using [frequent exports](/deploy/dgraph-administration/#exporting-database). ### Is Dgraph fast? Every other graph system that we've run it against, Dgraph has been at least a 10x factor faster. It only goes up from there. But, that's anecdotal observations. diff --git a/wiki/content/get-started/index.md b/wiki/content/get-started/index.md index 0614eb44936..a5a9f038269 100644 --- a/wiki/content/get-started/index.md +++ b/wiki/content/get-started/index.md @@ -5,6 +5,7 @@ aliases = ["/get-started-old"] url = "/get-started" name = "Get Started" identifier = "get-started" + parent = "dql" weight = 2 +++ diff --git a/wiki/content/graphql/_index.md b/wiki/content/graphql/_index.md new file mode 100644 index 00000000000..700780eaecb --- /dev/null +++ b/wiki/content/graphql/_index.md @@ -0,0 +1,7 @@ ++++ +title = "GraphQL" +[menu.main] + url = "/graphql/" + identifier = "graphql" + weight = 3 ++++ diff --git a/wiki/content/graphql/admin/index.md b/wiki/content/graphql/admin/index.md new file mode 100644 index 00000000000..6bdeb6ffa63 --- /dev/null +++ b/wiki/content/graphql/admin/index.md @@ -0,0 +1,140 @@ ++++ +title = "Admin" +[menu.main] + url = "/graphql/admin/" + name = "Admin" + identifier = "graphql-admin" + parent = "graphql" + weight = 12 ++++ + +The admin API and how to run Dgraph with GraphQL. + +## Running + +The simplest way to start with Dgraph GraphQL is to run the all-in-one Docker image. + +``` +docker run -it -p 8080:8080 dgraph/standalone:master +``` + +That brings up GraphQL at `localhost:8080/graphql` and `localhost:8080/admin`, but is intended for quickstart and doesn't persist data. + +## Advanced Options + +Once you've tried out Dgraph GraphQL, you'll need to move past the `dgraph/standalone` and run and deploy Dgraph instances. + +Dgraph is a distributed graph database. It can scale to huge data and shard that data across a cluster of Dgraph instances. GraphQL is built into Dgraph in its Alpha nodes. To learn how to manage and deploy a Dgraph cluster to build an App check Dgraph's [Dgraph docs](https://docs.dgraph.io/), and, in particular, the [deployment guide](https://docs.dgraph.io/deploy/). + +GraphQL schema introspection is enabled by default, but can be disabled with the `--graphql_introspection=false` when starting the Dgraph alpha nodes. + +## Dgraph's schema + +Dgraph's GraphQL runs in Dgraph and presents a GraphQL schema where the queries and mutations are executed in the Dgraph cluster. So the GraphQL schema is backed by Dgraph's schema. + +**Warning: this means if you have a Dgraph instance and change its GraphQL schema, the schema of the underlying Dgraph will also be changed!** + +## /admin + +When you start Dgraph with GraphQL, two GraphQL endpoints are served. + +At `/graphql` you'll find the GraphQL API for the types you've added. That's what your app would access and is the GraphQL entry point to Dgraph. If you need to know more about this, see the quick start, example and schema docs. + +At `/admin` you'll find an admin API for administering your GraphQL instance. The admin API is a GraphQL API that serves POST and GET as well as compressed data, much like the `/graphql` endpoint. + +Here are the important types, queries, and mutations from the admin schema. + +```graphql +type GQLSchema { + id: ID! + schema: String! + generatedSchema: String! +} + +type UpdateGQLSchemaPayload { + gqlSchema: GQLSchema +} + +input UpdateGQLSchemaInput { + set: GQLSchemaPatch! +} + +input GQLSchemaPatch { + schema: String! +} + +type Query { + getGQLSchema: GQLSchema + health: Health +} + +type Mutation { + updateGQLSchema(input: UpdateGQLSchemaInput!) : UpdateGQLSchemaPayload +} +``` + +You'll notice that the /admin schema is very much the same as the schemas generated by Dgraph GraphQL. + +* The `health` query lets you know if everything is connected and if there's a schema currently being served at `/graphql`. +* The `getGQLSchema` query gets the current GraphQL schema served at `/graphql`, or returns null if there's no such schema. +* The `updateGQLSchema` mutation allows you to change the schema currently served at `/graphql`. + +## First Start + +On first starting with a blank database: + +* There's no schema served at `/graphql`. +* Querying the `/admin` endpoint for `getGQLSchema` returns `"getGQLSchema": null`. +* Querying the `/admin` endpoint for `health` lets you know that no schema has been added. + +## Adding Schema + +Given a blank database, running the `/admin` mutation: + +```graphql +mutation { + updateGQLSchema( + input: { set: { schema: "type Person { name: String }"}}) + { + gqlSchema { + schema + generatedSchema + } + } +} +``` + +would cause the following. + +* The `/graphql` endpoint would refresh and now serves the GraphQL schema generated from type `type Person { name: String }`: that's Dgraph type `Person` and predicate `Person.name: string .`; see [here](/graphql/dgraph) for how to customize the generated schema. +* The schema of the underlying Dgraph instance would be altered to allow for the new `Person` type and `name` predicate. +* The `/admin` endpoint for `health` would return that a schema is being served. +* The mutation returns `"schema": "type Person { name: String }"` and the generated GraphQL schema for `generatedSchema` (this is the schema served at `/graphql`). +* Querying the `/admin` endpoint for `getGQLSchema` would return the new schema. + +## Migrating Schema + +Given an instance serving the schema from the previous section, running an `updateGQLSchema` mutation with the following input + +```graphql +type Person { + name: String @search(by: [regexp]) + dob: DateTime +} +``` + +changes the GraphQL definition of `Person` and results in the following. + +* The `/graphql` endpoint would refresh and now serves the GraphQL schema generated from the new type. +* The schema of the underlying Dgraph instance would be altered to allow for `dob` (predicate `Person.dob: datetime .` is added, and `Person.name` becomes `Person.name: string @index(regexp).`) and indexes are rebuilt to allow the regexp search. +* The `health` is unchanged. +* Querying the `/admin` endpoint for `getGQLSchema` now returns the updated schema. + +## Removing from Schema + +Adding a schema through GraphQL doesn't remove existing data (it would remove indexes). For example, starting from the schema in the previous section and running `updateGQLSchema` with the initial `type Person { name: String }` would have the following effects. + +* The `/graphql` endpoint would refresh to serve the schema built from this type. +* Thus field `dob` would no longer be accessible and there'd be no search available on `name`. +* The search index on `name` in Dgraph would be removed. +* The predicate `dob` in Dgraph is left untouched - the predicate remains and no data is deleted. diff --git a/wiki/content/graphql/api/_index.md b/wiki/content/graphql/api/_index.md new file mode 100644 index 00000000000..fbb4fc17d8c --- /dev/null +++ b/wiki/content/graphql/api/_index.md @@ -0,0 +1,8 @@ ++++ +title = "The API" +[menu.main] + url = "/graphql/api/" + identifier = "api" + parent = "graphql" + weight = 5 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/api/api-overview.md b/wiki/content/graphql/api/api-overview.md new file mode 100644 index 00000000000..7b977b9911b --- /dev/null +++ b/wiki/content/graphql/api/api-overview.md @@ -0,0 +1,23 @@ ++++ +title = "Overview" +[menu.main] + parent = "api" + identifier = "api-overview" + weight = 1 ++++ + +How to use the GraphQL API. + +Dgraph serves [spec-compliant +GraphQL](https://graphql.github.io/graphql-spec/June2018/) over HTTP to two endpoints: `/graphql` and `/admin`. + + +In Slash GraphQL `/graphql` and `/admin` are served from the domain of your backend, which will be something like `https://YOUR-SUBDOMAIN.REGION.aws.cloud.dgraph.io`. If you are running a self-hosted Dgraph instance that will be at the alpha port and url (which defaults to `http://localhost:8080` if you aren't changing any settings). + +In each case, both GET and POST requests are served. + +- `/graphql` is where you'll find the GraphQL API for the types you've added. That is the single GraphQL entry point for your apps to Dgraph. + +- `/admin` is where you'll find an admin API for administering your GraphQL instance. That's where you can update your GraphQL schema, perform health checks of your backend, and more. + +This section covers the API served at `/graphql`. See [Admin](/graphql/admin) to learn more about the admin API. diff --git a/wiki/content/graphql/api/errors.md b/wiki/content/graphql/api/errors.md new file mode 100644 index 00000000000..074981811bf --- /dev/null +++ b/wiki/content/graphql/api/errors.md @@ -0,0 +1,24 @@ ++++ +title = "GraphQL Error Propagation" +[menu.main] + parent = "api" + name = "GraphQL Errors" + weight = 6 ++++ + + + +Before returning query and mutation results, Dgraph uses the types in the schema to apply GraphQL [value completion](https://graphql.github.io/graphql-spec/June2018/#sec-Value-Completion) and [error handling](https://graphql.github.io/graphql-spec/June2018/#sec-Errors-and-Non-Nullability). That is, `null` values for non-nullable fields, e.g. `String!`, cause error propagation to parent fields. + +In short, the GraphQL value completion and error propagation mean the following. + +* Fields marked as nullable (i.e. without `!`) can return `null` in the json response. +* For fields marked as non-nullable (i.e. with `!`) Dgraph never returns null for that field. +* If an instance of type has a non-nullable field that has evaluated to null, the whole instance results in null. +* Reducing an object to null might cause further error propagation. For example, querying for a post that has an author with a null name results in null: the null name (`name: String!`) causes the author to result in null, and a null author causes the post (`author: Author!`) to result in null. +* Error propagation for lists with nullable elements, e.g. `friends [Author]`, can result in nulls inside the result list. +* Error propagation for lists with non-nullable elements results in null for `friends [Author!]` and would cause further error propagation for `friends [Author!]!`. + +Note that, a query that results in no values for a list will always return the empty list `[]`, not `null`, regardless of the nullability. For example, given a schema for an author with `posts: [Post!]!`, if an author has not posted anything and we queried for that author, the result for the posts field would be `posts: []`. + +A list can, however, result in null due to GraphQL error propagation. For example, if the definition is `posts: [Post!]`, and we queried for an author who has a list of posts. If one of those posts happened to have a null title (title is non-nullable `title: String!`), then that post would evaluate to null, the `posts` list can't contain nulls and so the list reduces to null. diff --git a/wiki/content/graphql/api/fragments.md b/wiki/content/graphql/api/fragments.md new file mode 100644 index 00000000000..8e4d9f05253 --- /dev/null +++ b/wiki/content/graphql/api/fragments.md @@ -0,0 +1,22 @@ ++++ +title = "GraphQL Fragements" +[menu.main] + parent = "api" + name = "GraphQL Fragements" + weight = 4 ++++ + +Documentation on GraphQL fragments is coming soon. + + \ No newline at end of file diff --git a/wiki/content/graphql/api/multiples.md b/wiki/content/graphql/api/multiples.md new file mode 100644 index 00000000000..b01d1007bf3 --- /dev/null +++ b/wiki/content/graphql/api/multiples.md @@ -0,0 +1,150 @@ ++++ +title = "Multiple GraphQL Operations in a Request" +[menu.main] + parent = "api" + name = "Multiple GraphQL Operations in a Request" + weight = 5 ++++ + +GraphQL requests can contain one or more operations. Operations are one of `query`, `mutation`, or `subscription`. If a request only has one operation, then it can be unnamed like the following: + +## Single Operation + +The most basic request contains a single anonymous (unnamed) operation. Each operation can have one or more queries within in. For example, the following query has `query` operation running the queries "getTask" and "getUser": + +```graphql +query { + getTask(id: "0x3") { + id + title + completed + } + getUser(username: "dgraphlabs") { + username + } +} +``` + +Response: + +```json +{ + "data": { + "getTask": { + "id": "0x3", + "title": "GraphQL docs example", + "completed": true + }, + "getUser": { + "username": "dgraphlabs" + } + } +} +``` + +You can optionally name the operation as well, though it's not required if the request only has one operation as it's clear what needs to be executed. + +### Query Shorthand + +If a request only has a single query operation, then you can use the short-hand form of omitting the "query" keyword: + +```graphql +{ + getTask(id: "0x3") { + id + title + completed + } + getUser(username: "dgraphlabs") { + username + } +} +``` + +This simplfies queries when a query doesn't require an operation name or [variables](/graphql/api/variables). + +## Multiple Operations + +If a request has two or more operations, then each operation must have a name. A request can only execute one operation, so you must also include the operation name to execute in the request (see the "operations" field for [requests](/graphql/api/requests)). Every operation name in a request must be unique. + +For example, in the following request has the operation names "getTaskAndUser" and "completedTasks". + +```graphql +query getTaskAndUser { + getTask(id: "0x3") { + id + title + completed + } + queryUser(filter: {username: {eq: "dgraphlabs"}}) { + username + name + } +} + +query completedTasks { + queryTask(filter: {completed: true}) { + title + completed + } +} +``` + +When executing the following request (as an HTTP POST request in JSON format), specifying the "getTaskAndUser" operation executes the first query: + +```json +{ + "query": "query getTaskAndUser { getTask(id: \"0x3\") { id title completed } queryUser(filter: {username: {eq: \"dgraphlabs\"}}) { username name }\n}\n\nquery completedTasks { queryTask(filter: {completed: true}) { title completed }}", + "operationName": "getTaskAndUser" +} +``` + +```json +{ + "data": { + "getTask": { + "id": "0x3", + "title": "GraphQL docs example", + "completed": true + }, + "queryUser": [ + { + "username": "dgraphlabs", + "name": "Dgraph Labs" + } + ] + } +} +``` + +And specifying the "completedTasks" operation executes the second query: + +```json +{ + "query": "query getTaskAndUser { getTask(id: \"0x3\") { id title completed } queryUser(filter: {username: {eq: \"dgraphlabs\"}}) { username name }\n}\n\nquery completedTasks { queryTask(filter: {completed: true}) { title completed }}", + "operationName": "completedTasks" +} +``` + +```json +{ + "data": { + "queryTask": [ + { + "title": "GraphQL docs example", + "completed": true + }, + { + "title": "Show second operation", + "completed": true + } + ] + } +} +``` + +## Additional Details + +When an operation contains multiple queries, they are run concurrently and independently in a Dgraph readonly transaction per query. + +When an operation contains multiple mutations, they are run serially, in the order listed in the request, and in a transaction per mutation. If a mutation fails, the following mutations are not executed, and previous mutations are not rolled back. diff --git a/wiki/content/graphql/api/requests.md b/wiki/content/graphql/api/requests.md new file mode 100644 index 00000000000..0e1e71c75e9 --- /dev/null +++ b/wiki/content/graphql/api/requests.md @@ -0,0 +1,205 @@ ++++ +title = "Requests and Responses" +[menu.main] + parent = "api" + name = "Requests and Responses" + weight = 2 ++++ + +In this section, we'll cover the structure for GraphQL requests and responses, how to enable compression for them, and configuration options for extensions. + +## Requests + +GraphQL requests can be sent via HTTP POST or HTTP GET requests. + +POST requests sent with the Content-Type header `application/graphql` must have a POST body content as a GraphQL query string. For example, the following is a valid POST body for a query: + +```graphql +query { + getTask(id: "0x3") { + id + title + completed + user { + username + name + } + } +} +``` + +POST requests sent with the Content-Type header `application/json` must have a POST body in the following JSON format: + +```json +{ + "query": "...", + "operationName": "...", + "variables": { "var": "val", ... } +} +``` + +GET requests must be sent in the following format. The query, variables, and operation are sent as URL-encoded query parameters in the URL. + +``` +http://localhost:8080/graphql?query={...}&variables={...}&operation=... +``` + +In either request method (POST or GET), only `query` is required. `variables` is only required if the query contains GraphQL variables: i.e. the query starts like `query myQuery($var: String)`. `operationName` is only required if there are multiple operations in the query; in which case, operations must also be named. + +## Responses + +GraphQL responses are in JSON. Every response is a JSON map, and will include JSON keys for `"data"`, `"errors"`, or `"extensions"` following the GraphQL specification. They follow the following formats. + +Successful queries are in the following format: + +```json +{ + "data": { ... }, + "extensions": { ... } +} +``` + +Queries that have errors are in the following format. + +```json +{ + "errors": [ ... ], +} +``` + +All responses, including errors, always return HTTP 200 OK status codes. An error response will contain an `"errors"` field. + +### "data" field + +The "data" field contains the result of your GraphQL request. The response has exactly the same shape as the result. For example, notice that for the following query, the response includes the data in the exact shape as the query. + +Query: + +```graphql +query { + getTask(id: "0x3") { + id + title + completed + user { + username + name + } + } +} +``` + +Response: + +```json +{ + "data": { + "getTask": { + "id": "0x3", + "title": "GraphQL docs example", + "completed": true, + "user": { + "username": "dgraphlabs", + "name": "Dgraph Labs" + } + } + } +} +``` + +### "errors" field + +The "errors" field is a JSON list where each entry has a `"message"` field that describes the error and optionally has a `"locations"` array to list the specific line and column number of the request that points to the error described. For example, here's a possible error for the following query, where `getTask` needs to have an `id` specified as input: + +Query: +```graphql +query { + getTask() { + id + } +} +``` + +Response: +```json +{ + "errors": [ + { + "message": "Field \"getTask\" argument \"id\" of type \"ID!\" is required but not provided.", + "locations": [ + { + "line": 2, + "column": 3 + } + ] + } + ] +} +``` + +### "extensions" field + +The "extensions" field contains extra metadata for the request with metrics and trace information for the request. + +- `"touched_uids"`: The number of nodes that were touched to satisfy the request. This is a good metric to gauge the complexity of the query. +- `"tracing"`: Displays performance tracing data in [Apollo Tracing][apollo-tracing] format. This includes the duration of the whole query and the duration of each operation. + +[apollo-tracing]: https://github.com/apollographql/apollo-tracing + +Here's an example of a query response with the extensions field: + +```json +{ + "data": { + "getTask": { + "id": "0x3", + "title": "GraphQL docs example", + "completed": true, + "user": { + "username": "dgraphlabs", + "name": "Dgraph Labs" + } + } + }, + "extensions": { + "touched_uids": 9, + "tracing": { + "version": 1, + "startTime": "2020-07-29T05:54:27.784837196Z", + "endTime": "2020-07-29T05:54:27.787239465Z", + "duration": 2402299, + "execution": { + "resolvers": [ + { + "path": [ + "getTask" + ], + "parentType": "Query", + "fieldName": "getTask", + "returnType": "Task", + "startOffset": 122073, + "duration": 2255955, + "dgraph": [ + { + "label": "query", + "startOffset": 171684, + "duration": 2154290 + } + ] + } + ] + } + } + } +} +``` + +### Turn off extensions + +Extensions are returned in every response. These are completely optional. If you'd like to turn off extensions, you can set the config option `--graphql_extensions=false` in Dgraph Alpha. + +## Compression + +By default, requests and responses are not compressed. Typically, enabling compression saves from sending additional data to and from the backend while using a bit of extra processing time to do the compression. + +You can turn on compression for requests and responses by setting the standard HTTP headers. To send compressed requests, set HTTP header `Content-Encoding` to `gzip` to POST gzip-compressed data. To receive compressed responses, set the HTTP header `Accept-Encoding` to `gzip` in your request. diff --git a/wiki/content/graphql/api/variables.md b/wiki/content/graphql/api/variables.md new file mode 100644 index 00000000000..79e9333a330 --- /dev/null +++ b/wiki/content/graphql/api/variables.md @@ -0,0 +1,10 @@ ++++ +title = "GraphQL Variables" +[menu.main] + parent = "api" + name = "GraphQL Variables" + identifier = "graphql-variables" + weight = 3 ++++ + +Docs on using GraphQL variables in queries and mutations is coming soon. \ No newline at end of file diff --git a/wiki/content/graphql/authorization/_index.md b/wiki/content/graphql/authorization/_index.md new file mode 100644 index 00000000000..452cb49a5bc --- /dev/null +++ b/wiki/content/graphql/authorization/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Authorization" +[menu.main] + url = "/graphql/authorization/" + identifier = "authorization" + parent = "graphql" + weight = 10 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/authorization/authorization-overview.md b/wiki/content/graphql/authorization/authorization-overview.md new file mode 100644 index 00000000000..1a0305a5205 --- /dev/null +++ b/wiki/content/graphql/authorization/authorization-overview.md @@ -0,0 +1,56 @@ ++++ +title = "Overview" +[menu.main] + parent = "authorization" + identifier = "authorization-overview" + weight = 1 ++++ + +Dgraph GraphQL comes with inbuilt authorization. It allows you to annotate your schema with rules that determine who can access or mutate what data. + +Firstly, let's get some concepts defined. There are two important concepts in what's often called 'auth'. + +* authentication : who are you; and +* authorization : what are you allowed to do. + +Dgraph GraphQL deals with authorization, but is completely flexible about how your app does authentication. You could authenticate your users with a cloud service like OneGraph or Auth0, use some social sign in options, or write bespoke code. The connection between Dgraph and your authentication mechanism is a signed JWT - you tell Dgraph, for example, the public key of the JWT signer and Dgraph trusts JWTs signed by the corresponding private key. + +With an authentication mechanism set up, you then annotate your schema with the `@auth` directive to define your authorization rules, attach details of your authentication provider to the last line of the schema, and pass the schema to Dgraph. So your schema will follow this pattern. + +```graphql +type A @auth(...) { + ... +} + +type B @auth(...) { + ... +} + +# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"","Algo":"","Audience":[]} +``` + +* `Header` is the header in which requests will send the signed JWT +* `Namespace` is the key inside the JWT that contains the claims relevant to Dgraph auth +* `Algo` is JWT verification algorithm which can be either `HS256` or `RS256`, and +* `VerificationKey` is the string value of the key (newlines replaced with `\n`) wrapped in `""` +* `Audience` is used to verify `aud` field of JWT which might be set by certain providers. It indicates the intended audience for the JWT. This is an optional field. + +Valid examples look like + +`# Dgraph.Authorization {"VerificationKey":"verificationkey","Header":"X-My-App-Auth","Namespace":"https://my.app.io/jwt/claims","Algo":"HS256","Audience":["aud1","aud5"]}` + +Without audience field + +`# Dgraph.Authorization {"VerificationKey":"secretkey","Header":"X-My-App-Auth","Namespace":"https://my.app.io/jwt/claims","Algo":"HS256"}` + +for HMAC-SHA256 JWT with symmetric cryptography (the signing key and verification key are the same), and like + +`# Dgraph.Authorization {"VerificationKey":"-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----","Header":"X-My-App-Auth","Namespace":"https://my.app.io/jwt/claims","Algo":"RS256"}` + +for RSA Signature with SHA-256 asymmetric cryptography (the JWT is signed with the private key and Dgraph checks with the public key). + +Both cases expect the JWT to be in a header `X-My-App-Auth` and expect the JWT to contain custom claims object `"https://my.app.io/jwt/claims": { ... }` with the claims used in authorization rules. + +Note: authorization is in beta and some aspects may change - for example, it's possible that the method to specify the header, key, etc. will move into the /admin `updateGQLSchema` mutation that sets the schema. Some features are also in active improvement and development - for example, auth is supported an on types, but interfaces (and the types that implement them) don't correctly support auth in the current beta. + +--- diff --git a/wiki/content/graphql/authorization/directive.md b/wiki/content/graphql/authorization/directive.md new file mode 100644 index 00000000000..744c412f08e --- /dev/null +++ b/wiki/content/graphql/authorization/directive.md @@ -0,0 +1,206 @@ ++++ +title = "The `@auth` directive" +[menu.main] + parent = "authorization" + weight = 2 ++++ + +Given an authentication mechanism and signed JWT, it's the `@auth` directive that tells Dgraph how to apply authorization. The directive can be used on any type (that isn't a `@remote` type) and specifies the authorization for `query` as well as `add`, `update` and `delete` mutations. + +In each case, `@auth` specifies rules that Dgraph applies during queries and mutations. Those rules are expressed in exactly the same syntax as GraphQL queries. Why? Because the authorization you add to your app is about the graph of your application, so graph rules make sense. It's also the syntax you already know about, you get syntax help from GraphQL tools in writing such rules, and it turns out to be exactly the kinds of rules Dgraph already knows how to evaluate. + +Here's how the rules work. + +## Authorization rules + +A valid type and rule looks like the following. + +```graphql +type Todo @auth( + query: { rule: """ + query ($USER: String!) { + queryTodo(filter: { owner: { eq: $USER } } ) { + id + } + }""" + } +){ + id: ID! + text: String! @search(by: [term]) + owner: String! @search(by: [hash]) +} +``` + +In addition to it, details of the authentication provider should be given in the last line of the schema, as discussed in section [authorization-overview](/graphql/authorization/authorization-overview). + +Here we define a type `Todo`, that's got an `id`, the `text` of the todo and the username of the `owner` of the todo. What todos can a user query? Any `Todo` that the `query` rule would also return. + +The `query` rule in this case expects the JWT to contain a claim `"USER": "..."` giving the username of the logged in user, and says: you can query any todo that has your username as the owner. + +In this example we use the `queryTodo` query that will be auto generated after uploading this schema. When using a query in a rule, you can only use the `queryTypeName` query. Where `TypeName` matches the name of the type, where the `@auth` directive is attached. In other words, we could not have used the `getTodo` query in our rule above to query by id only. + +This rule is applied automatically at query time. For example, the query + +```graphql +query { + queryTodo { + id + text + } +} +``` + +will return only the todo's where `owner` equals `amit`, when Amit is logged in and only the todos owned by `nancy` when she's logged into your app. + +Similarly, + +```graphql +query { + queryTodo(filter: { text: { anyofterms: "graphql"}}, first: 10, order: { asc: text }) { + id + text + } +} +``` + +will return the first ten todos, ordered ascending by title of the user that made the query. + +This means your frontend doesn't need to be sensitive to the auth rules. Your app can simply query for the todos and that query behaves properly depending on who's logged in. + +In general, an auth rule should select a field that's expected to exist at the inner most field, often that's the `ID` or `@id` field. Auth rules are run in a mode that requires all fields in the rule to find a value in order to succeed. + +## Graph traversal in auth rules + +Often authorization depends not on the object being queried, but on the connections in the graph that object has or doesn't have. Because the auth rules are graph queries, they can express very powerful graph search and traversal. + +For a simple todo app, it's more likely that you'll have types like this: + +```graphql +type User { + username: String! @id + todos: [Todo] +} + +type Todo { + id: ID! + text: String! + owner: User +} +``` + +This means your auth rule for todos will depend not on a value in the todo, but on checking which owner it's linked to. This means our auth rule must make a step further into the graph to check who the owner is. + +```graphql +query ($USER: String!) { + queryTodo { + owner(filter: { username: { eq: $USER } } ) { + username + } + } +} +``` + +You can express a lot with these kinds of graph traversals. For example, multitenancy rules can express that you can only see an object if it's linked (through what ever graph search you define) to the organization you were authenticated from. That means your app can split data per customer easily. + +You can also express rules that can be administered by the app itself. You might define type `Role` and enum `Privileges` that can have values like `VIEW`, `ADD`, etc. and state in your auth rules that a user needs to have a role with particular privileges to query/add/update/delete and those roles can then be allocated inside the app. For example, in an app about project management, when a project is created the admin can decide which users have view or edit permission, etc. + +## Role Based Access Control + +As well as rules that relate a user's claims to a graph traversal, role based access control rules are also possible. These rules relate a claim in the JWT to a known value. + +For example, perhaps only someone logged in with the `ADMIN` role is allowed to delete users. For that, we might expect the JWT to contain a claim `"ROLE": "ADMIN"`, and can thus express a rule that only allows users with the `ADMIN` claim to delete. + +```graphql +type User @auth( + delete: { rule: "{$ROLE: { eq: \"ADMIN\" } }"} +) { + username: String! @id + todos: [Todo] +} +``` + +Not all claims need to be present in all JWTs. For example, if the `ROLE` claim isn't present in a JWT, any rule that relies on `ROLE` simply evaluates to false. As well as simplifying your JWTs (e.g. not all users need a role if it doesn't make sense to do so), this means you can also simply disallow some queries and mutations. If you know that your JWTs never contain the claim `DENIED`, then a rule such as + +```graphql +type User @auth( + delete: { rule: "{$DENIED: { eq: \"DENIED\" } }"} +) { + ... +} +``` + +can never be true and this would prevent users ever being deleted. + +## and, or & not + +Rules can be combined with the logical connectives and, or and not, so a permission can be a mixture of graph traversals and role based rules. + +In the todo app, you can express, for example, that you can delete a `Todo` if you are the author, or are the site admin. + +```graphql +type Todo @auth( + delete: { or: [ + { rule: "query ($USER: String!) { ... }" }, # you are the author graph query + { rule: "{$ROLE: { eq: \"ADMIN\" } }" } + ]} +) +``` + +## Public Data + +Many apps have data that can be accessed by anyone, logged in or not. That also works nicely with Dgraph auth rules. + +For example, in Twitter, StackOverflow, etc. you can see authors and posts without being signed it - but you'd need to be signed in to add a post. With Dgraph auth rules, if a type doesn't have, for example, a `query` auth rule or the auth rule doesn't depend on a JWT value, then the data can be accessed without a signed JWT. + +For example, the todo app might allow anyone, logged in or not, to view any author, but not make any mutations unless logged in as the author or an admin. That would be achieved by rules like the following. + +```graphql +type User @auth( + # no query rule + add: { rule: "{$ROLE: { eq: \"ADMIN\" } }" }, + update: ... + delete: ... +) { + username: String! @id + todos: [Todo] +} +``` + +Maybe some todos can be marked as public and users you aren't logged in can see those. + +```graphql +type Todo @auth( + query: { or: [ + # you are the author + { rule: ... }, + # or, the todo is marked as public + { rule: """query { + queryTodo(filter: { isPublic: { eq: true } } ) { + id + } + }"""} + ]} +) { + ... + isPublic: Boolean +} + +``` + +Because the rule doesn't depend on a JWT value, it can be successfully evaluated for users who aren't logged in. + +Ensuring that requests are from an authenticated JWT, and no further restrictions, can be done by arranging the JWT to contain a value like `"isAuthenticated": "true"`. For example, + + +```graphql +type User @auth( + query: { rule: "{$isAuthenticated: { eq: \"true\" } }" }, +) { + username: String! @id + todos: [Todo] +} +``` + +specifies that only authenticated users can query other users. + +--- diff --git a/wiki/content/graphql/authorization/mutations.md b/wiki/content/graphql/authorization/mutations.md new file mode 100644 index 00000000000..a8c4b6acd78 --- /dev/null +++ b/wiki/content/graphql/authorization/mutations.md @@ -0,0 +1,111 @@ ++++ +title = "Mutations" +[menu.main] + parent = "authorization" + weight = 3 ++++ + +Mutations with auth work similarly to query. However, mutations involve a state change in the database, so it's important to understand when the rules are applied and what they mean. + +## Add + +Rules for `add` authorization state that the rule must hold of nodes created by the mutation data once committed to the database. + +For example, a rule such as: + +```graphql +type Todo @auth( + add: { rule: """ + query ($USER: String!) { + queryTodo { + owner(filter: { username: { eq: $USER } } ) { + username + } + } + }""" + } +){ + id: ID! + text: String! + owner: User +} +type User { + username: String! @id + todos: [Todo] +} +``` + +states that if you add a new todo, then that new todo must be a todo that satisfies the `add` rule, in this case saying that you can only add todos with yourself as the author. + +## Delete + +Delete rules filter the nodes that can be deleted. A user can only ever delete a subset of the nodes that the `delete` rules allow. + +For example, this rule states that a user can delete a todo if they own it, or they have the `ADMIN` role. + +```graphql +type Todo @auth( + delete: { or: [ + { rule: """ + query ($USER: String!) { + queryTodo { + owner(filter: { username: { eq: $USER } } ) { + username + } + } + }""" + }, + { rule: "{$ROLE: { eq: \"ADMIN\" } }"} + ]} +){ + id: ID! + text: String! @search(by: [term]) + owner: User +} + +type User { + username: String! @id + todos: [Todo] +} +``` + +So a mutation like: + +```graphql +mutation { + deleteTodo(filter: { text: { anyofterms: "graphql" } }) { + numUids + } +} +``` + +for most users would delete their own posts that contain the term "graphql", but wouldn't affect any other user's todos; for an admin, it would delete any users posts that contain "graphql" + +For add, what matters is the resulting state of the database, for delete it's the state before the delete occurs. + +## Update + +Updates have both a before and after state that can be important for auth. + +For example, consider a rule stating that you can only update your own todos. If evaluated in the database before the mutation, like the delete rules, it would prevent you updating anyone elses todos, but does it stop you updating your own todo to have a different `owner`. If evaluated in the database after the mutation occurs, like for add rules, it would stop setting the `owner` to another user, but would it prevent editing other's posts. + +Currently, Dgraph evaluates `update` rules _before_ the mutation. Our auth support is still in beta and we may extend this for example to make the `update` rule an invariant of the mutation, or enforce pre and post conditions, or even allow custom logic to validate the update data. + +## Update and Add + +Update mutations can also insert new data. For example, you might allow a mutation that runs an update mutation to add a new todo. + +```graphql +mutation { + updateUser(input: { + filter: { username: { eq: "aUser" }}, + set: { todos: [ { text: "do this new todo"} ] } + }) { + ... + } +} +``` + +Such a mutation updates a user's todo list by inserting a new todo. It would have to satisfy the rules to update the author _and_ the rules to add a todo. If either fail, the mutation has no effect. + +--- \ No newline at end of file diff --git a/wiki/content/graphql/custom/_index.md b/wiki/content/graphql/custom/_index.md new file mode 100644 index 00000000000..08c6e0a7c61 --- /dev/null +++ b/wiki/content/graphql/custom/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Custom Resolvers" +[menu.main] + url = "/graphql/custom/" + identifier = "custom" + parent = "graphql" + weight = 9 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/custom/custom-overview.md b/wiki/content/graphql/custom/custom-overview.md new file mode 100644 index 00000000000..7dcf5f2b4b9 --- /dev/null +++ b/wiki/content/graphql/custom/custom-overview.md @@ -0,0 +1,56 @@ ++++ +title = "Overview" +[menu.main] + parent = "custom" + identifier = "custom-resolvers-overview" + weight = 1 ++++ + +Dgraph creates a GraphQL API from nothing more than GraphQL types. That's great, and gets you moving fast from an idea to a running app. However, at some point, as your app develops, you might want to customize the behaviour of your schema. + +In Dgraph, you do that with code (in any language you like) that implements custom resolvers. + +Dgraph doesn't execute your custom logic itself. It makes external HTTP requests. That means, you can deploy your custom logic into the same Kubernetes cluster as your Dgraph instance, deploy and call, for example, AWS Lambda functions, or even make calls to existing HTTP and GraphQL endpoints. + +## The `@custom` directive + +There are three places you can use the `@custom` directive and thus tell Dgraph where to apply custom logic. + +1) You can add custom queries to the Query type + +```graphql +type Query { + myCustomQuery(...): QueryResultType @custom(...) +} +``` + +2) You can add custom mutations to the Mutation type + +```graphql +type Mutation { + myCustomMutation(...): MutationResult @custom(...) +} +``` + +3) You can add custom fields to your types + +```graphql +type MyType { + ... + customField: FieldType @custom(...) + ... +} +``` + +## Learn more + +Find out more about the `@custom` directive [here](/graphql/custom/directive), or check out: + +* [custom query examples](/graphql/custom/query) +* [custom mutation examples](/graphql/custom/mutation), or +* [custom field examples](/graphql/custom/field) + + + + +--- diff --git a/wiki/content/graphql/custom/directive.md b/wiki/content/graphql/custom/directive.md new file mode 100644 index 00000000000..355063e927b --- /dev/null +++ b/wiki/content/graphql/custom/directive.md @@ -0,0 +1,365 @@ ++++ +title = "The `@custom` directive" +[menu.main] + parent = "custom" + weight = 2 ++++ + +The `@custom` directive is used to define custom queries, mutations and fields. + +In all cases, the result type (of the query, mutation or field) can be either: + +* a type that's stored in Dgraph (that's any type you've defined in your schema), or +* a type that's not stored in Dgraph and is marked with the `@remote` directive. + +Because the result types can be local or remote, you can call other HTTP endpoints, call remote GraphQL, or even call back to your Dgraph instance to add extra logic on top of Dgraph's graph search or mutations. + +Here's the GraphQL definition of the directives: + +```graphql +directive @custom(http: CustomHTTP) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +enum HTTPMethod { GET POST PUT PATCH DELETE } +enum Mode { SINGLE BATCH } +``` + +Each definition of custom logic must include: + +* the `url` where the custom logic is called. This can include a path and parameters that depend on query/mutation arguments or other fields. +* the HTTP `method` to use in the call. For example, when calling a REST endpoint with `GET`, `POST`, etc. + +Optionally, the custom logic definition can also include: + +* a `body` definition that can be used to construct a HTTP body from from arguments or fields. +* a list of `forwardHeaders` to take from the incoming request and add to the outgoing HTTP call. +Used, for example, if the incoming request contains an auth token that must be passed to the custom logic. +* a list of `secretHeaders` to take from the `Dgraph.Secret` defined in the schema file and add to the outgoing HTTP call. +Used, for example, for a server side API key and other static value that must be passed to the custom logic. +* the `graphql` query/mutation to call if the custom logic is a GraphQL server and whether to introspect or not (`skipIntrospection`) the remote GraphQL endpoint. +* `mode` which is used for resolving fields by calling an external GraphQL query/mutation. It can either be `BATCH` or `SINGLE`. +* a list of `introspectionHeaders` to take from the `Dgraph.Secret` defined in the schema file and added to the +introspection requests sent to the `graphql` query/mutation. + + +The result type of custom queries and mutations can be any object type in your schema, including `@remote` types. For custom fields the type can be object types or scalar types. + +The `method` can be any of the HTTP methods: `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`, and `forwardHeaders` is a list of headers that should be passed from the incoming request to the outgoing HTTP custom request. Let's look at each of the other `http` arguments in detail. + +## Dgraph.Secret + +Sometimes you might want to forward some static headers to your custom API which can't be exposed +to the client. This could be an API key from a payment processor or an auth token for your organization +on GitHub. These secrets can be specified as comments in the schema file and then can be used in +`secretHeaders` and `introspectionHeaders` while defining the custom directive for a field/query. + + +```graphql + type Query { + getTopUsers(id: ID!): [User] @custom(http: { + url: "http://api.github.com/topUsers", + method: "POST", + introspectionHeaders: ["Github-Api-Token"], + secretHeaders: ["Authorization:Github-Api-Token"], + graphql: "..." + }) +} + +# Dgraph.Secret Github-Api-Token "long-token" +``` + +In the above request, `Github-Api-Token` would be sent as a header with value `long-token` for +the introspection request. For the actual request, the value `Authorization` would be sent along with +the value `long-token`. Note `Authorization:Github-Api-Token` syntax tells us to use the value for the +`Github-Api-Token` dgraph secret but to forward it to the custom API with the header key as `Authorization`. + + +## The URL and method + +The URL can be as simple as a fixed URL string, or include details drawn from the arguments or fields. + +A simple string might look like: + +```graphql +type Query { + myCustomQuery: MyResult @custom(http: { + url: "https://my.api.com/theQuery", + method: GET + }) +} +``` + +While, in more complex cases, the arguments of the query/mutation can be used as a pattern for the URL: + +```graphql +type Query { + myGetPerson(id: ID!): Person @custom(http: { + url: "https://my.api.com/person/$id", + method: GET + }) + + getPosts(authorID: ID!, numToFetch: Int!): [Post] @custom(http: { + url: "https://my.api.com/person/$authorID/posts?limit=$numToFetch", + method: GET + }) +} +``` + +In this case, a query like + +```graphql +query { + getPosts(authorID: "auth123", numToFetch: 10) { + title + } +} +``` + +gets transformed to an outgoing HTTP GET request to the URL `https://my.api.com/person/auth123/posts?limit=10`. + +When using custom logic on fields, the URL can draw from other fields in the type. For example: + +```graphql +type User { + username: String! @id + ... + posts: [Post] @custom(http: { + url: "https://my.api.com/person/$username/posts", + method: GET + }) +} +``` + +Note that: + +* Fields or arguments used in the path of a URL, such as `username` or `authorID` in the exapmles above, must be marked as non-nullable (have `!` in their type); whereas, those used in parameters, such as `numToFetch`, can be nullable. +* Currently, only scalar fields or arguments are allowed to be used in URLs or bodies; though, see body below, this doesn't restrict the objects you can construct and pass to custom logic functions. +* Currently, the body can only contain alphanumeric characters in the key and other characters like `_` are not yet supported. +* Currently, constant values are not also not allowed in the body template. This would soon be supported. + +## The body + +Many HTTP requests, such as add and update operations on REST APIs, require a JSON formatted body to supply the data. In a similar way to how `url` allows specifying a url pattern to use in resolving the custom request, Dgraph allows a `body` pattern that is used to build HTTP request bodies. + +For example, this body can be structured JSON that relates a mutation's arguments to the JSON structure required by the remote endpoint. + +```graphql +type Mutation { + newMovie(title: String!, desc: String, dir: ID, imdb: ID): Movie @custom(http: { + url: "http://myapi.com/movies", + method: "POST", + body: "{ title: $title, imdbID: $imdb, storyLine: $desc, director: { id: $dir }}", + }) +``` + +A request with `newMovie(title: "...", desc: "...", dir: "dir123", imdb: "tt0120316")` is transformed into a `POST` request to `http://myapi.com/movies` with a JSON body of: + +```json +{ + "title": "...", + "imdbID": "tt0120316", + "storyLine": "...", + "director": { + "id": "dir123" + } +} +``` + +`url` and `body` templates can be used together in a single custom definition. + +For both `url` and `body` templates, any non-null arguments or fields must be present to evaluate the custom logic. And the following rules are applied when building the request from the template for nullable arguments or fields. + +* If the value of a nullable argument is present, it's used in the template. +* If a nullable argument is present, but null, then in a body `null` is inserted, while in a url nothing is added. For example, if the `desc` argument above is null then `{ ..., storyLine: null, ...}` is constructed for the body. Whereas, in a URL pattern like `https://a.b.c/endpoint?arg=$gqlArg`, if `gqlArg` is present, but null, the generated URL is `https://a.b.c/endpoint?arg=`. +* If a nullable argument is not present, nothing is added to the URL/body. That would mean the constructed body would not contain `storyLine` if the `desc` argument is missing, and in `https://a.b.c/endpoint?arg=$gqlArg` the result would be `https://a.b.c/endpoint` if `gqlArg` were not present in the request arguments. + +## Calling GraphQL custom resolvers + +Custom queries, mutations and fields can be implemented by custom GraphQL resolvers. In this case, use the `graphql` argument to specify which query/mutation on the remote server to call. The syntax includes if the call is a query or mutation, the arguments, and what query/mutation to use on the remote endpoint. + +For example, you can pass arguments to queries onward as arguments to remote GraphQL endpoints: + +```graphql +type Query { + getPosts(authorID: ID!, numToFetch: Int!): [Post] @custom(http: { + url: "https://my.api.com/graphql", + method: POST, + graphql: "query($authorID: ID!, $numToFetch: Int!) { posts(auth: $authorID, first: $numToFetch) }" + }) +} +``` + +You can also define your own inputs and pass those to the remote GraphQL endpoint. + +```graphql +input NewMovieInput { ... } + +type Mutation { + newMovie(input: NewMovieInput!): Movie @custom(http: { + url: "http://movies.com/graphql", + method: "POST", + graphql: "mutation($input: NewMovieInput!) { addMovie(data: $input) }", + }) +``` + +When a schema is uploaded, Dgraph will try to introspect the remote GraphQL endpoints on any custom logic that uses the `graphql` argument. From the results of introspection, it tries to match up arguments, input and object types to ensure that the calls to and expected responses from the remote GraphQL make sense. + +If that introspection isn't possible, set `skipIntrospection: true` in the custom definition and Dgraph won't perform GraphQL schema introspection for this custom definition. + +## Remote types + +Any type annotated with the `@remote` directive is not stored in Dgraph. This allows your Dgraph GraphQL instance to serve an API that includes both data stored locally and data stored or generated elsewhere. You can also use custom fields, for example, to join data from disparate datasets. + +Remote types can only be returned by custom resolvers and Dgraph won't generate any search or CRUD operations for remote types. + +The schema definition used to define your Dgraph GraphQL API must include definitions of all the types used. If a custom logic call returns a type not stored in Dgraph, then that type must be added to the Dgraph schema with the `@remote` directive. + +For example, you api might use custom logic to integrate with GitHub, using either `https://api.github.com` or the GitHub GraphQL api `https://api.github.com/graphql` and calling the `user` query. Either way, your GraphQL schema will need to include the type you expect back from that remote call. That could be linking a `User` as stored in your Dgraph instance with the `Repository` data from GitHub. With `@remote` types, that's as simple as adding the type and custom call to your schema. + +```graphql +# GitHub's repository type +type Respository @remote { ... } + +# Dgraph user type +type User { + # local user name = GitHub id + username: String! @id + + # ... + # other data stored in Dgraph + # ... + + # join local data with remote + repositories: [Repository] @custom(http: { + url: "https://api.github.com/users/$username/repos", + method: GET + }) +} +``` + +Just defining the connection is all it takes and then you can ask a single GraphQL query that performs a local query and joins with (potentialy many) remote data sources. + +## How Dgraph processes custom results + +Given types like + +```graphql +type Post @remote { + id: ID! + title: String! + datePublished: DateTime + author: Author +} + +type Author { ... } +``` + +and a custom query + +```graphql +type Query { + getCustomPost(id: ID!): Post @custom(http: { + url: "https://my.api.com/post/$id", + method: GET + }) + + getPosts(authorID: ID!, numToFetch: Int!): [Post] @custom(http: { + url: "https://my.api.com/person/$authorID/posts?limit=$numToFetch", + method: GET + }) +} +``` + +Dgraph turns the `getCustomPost` query into a HTTP request to `https://my.api.com/post/$id` and expects a single JSON object with fields `id`, `title`, `datePublished` and `author` as result. Any additional fields are ignored, while if non-nullable fields (like `id` and `title`) are missing, GraphQL error propagation will be triggered. + +For `getPosts`, Dgraph expects the HTTP call to `https://my.api.com/person/$authorID/posts?limit=$numToFetch` to return a JSON array of JSON objects, with each object matching the `Post` type as described above. + +If the custom resolvers are GraphQL calls, like: + +```graphql +type Query { + getCustomPost(id: ID!): Post @custom(http: { + url: "https://my.api.com/graphql", + method: POST, + graphql: "query(id: ID) { post(postID: $id) }" + }) + + getPosts(authorID: ID!, numToFetch: Int!): [Post] @custom(http: { + url: "https://my.api.com/graphql", + method: POST, + graphql: "query(id: ID) { postByAuthor(authorID: $id, first: $numToFetch) }" + }) +} +``` + +then Dgraph expects a GraphQL call to `post` to return a valid GraphQL result like `{ "data": { "post": {...} } }` and will use the JSON object that is the value of `post` as the data resolved by the request. + +Similarly, Dgraph expects `postByAuthor` to return data like `{ "data": { "postByAuthor": [ {...}, ... ] } }` and will use the array value of `postByAuthor` to build its array of posts result. + + +## How custom fields are resolved + +When evaluating a request that includes custom fields, Dgraph might run multiple resolution stages to resolve all the fields. Dgraph must also ensure it requests enough data to forfull the custom fields. For example, given the `User` type defined as: + +```graphql +type User { + username: String! @id + ... + posts: [Post] @custom(http: { + url: "https://my.api.com/person/$username/posts", + method: GET + }) +} +``` + +a query such as: + +```graphql +query { + queryUser { + username + posts + } +} +``` + +is executed by first querying in Dgraph for `username` and then using the result to resolve the custom field `posts` (which relies on `username`). For a request like: + +```graphql +query { + queryUser { + posts + } +} +``` + +Dgraph works out that it must first get `username` so it can run the custom field `posts`, even though `username` isn't part of the original query. So Dgraph retrieves enough data to satisfy the custom request, even if that involves data that isn't asked for in the query. + +There are currently a few limitations on custom fields: + +* each custom call must include either an `ID` or `@id` field +* arguments are not allowed (soon custom field arguments will be allowed and will be used in the `@custom` directive in the same manner as for custom queries and mutations), and +* a custom field can't depend on another custom field (longer term, we intend to lift this restriction). + +## Restrictions / Roadmap + +Our custom logic is still in beta and we are improving it quickly. Here's a few points that we plan to work on soon: + +* adding arguments to custom fields +* relaxing the restrictions on custom fields using id values +* iterative evaluation of `@custom` and `@remote` - in the current version you can't have `@custom` inside an `@remote` type once we add this, you'll be able to extend remote types with custom fields, and +* allowing fine tuning of the generated API, for example removing of customizing the generated CRUD mutations. + +--- diff --git a/wiki/content/graphql/custom/field.md b/wiki/content/graphql/custom/field.md new file mode 100644 index 00000000000..3c77d1109fc --- /dev/null +++ b/wiki/content/graphql/custom/field.md @@ -0,0 +1,82 @@ ++++ +title = "Custom Fields" +[menu.main] + parent = "custom" + weight = 5 ++++ + +Custom fields allow you to extend your types with custom logic as well as make joins between your local data and remote data. + +Let's say we are building an app for managing projects. Users will login with their GitHub id and we want to connect some data about their work stored in Dgraph with say their GitHub profile, issues, etc. + +Our first version of our users might start out with just their GitHub username and some data about what projects they are working on. + +```graphql +type User { + username: String! @id + projects: [Project] + tickets: [Ticket] +} +``` + +We can then add their GitHub repositories by just extending the definitions with the types and custom field needed to make the remote call. + +```graphql +# GitHub's repository type +type Repository @remote { ... } + +# Dgraph user type +type User { + # local user name = GitHub id + username: String! @id + + # join local data with remote + repositories: [Repository] @custom(http: { + url: "https://api.github.com/users/$username/repos", + method: GET + }) +} +``` + +We could similarly join with say the GitHub user details, or open pull requests, to further fill out the join between GitHub and our local data. Instead of the REST API, let's use the GitHub GraphQL endpoint + + +```graphql +# GitHub's User type +type GitHubUser @remote { ... } + +# Dgraph user type +type User { + # local user name = GitHub id + username: String! @id + + # join local data with remote + gitDetails: GitHubUser @custom(http: { + url: "https://api.github.com/graphql", + method: POST, + graphql: "query(username: String!) { user(login: $username) }", + skipIntrospection: true + }) +} +``` + +Perhaps our app has some measure of their volocity that's calculated by a custom function that looks at both their GitHub commits and some other places where work is added. Soon we'll have a schema where we can render a user's home page, the projects they work on, their open tickets, their GitHub details, etc. in a single request that queries across multiple sources and can mix Dgraph filtering with external calls. + +```graphql +query { + getUser(id: "aUser") { + username + projects(order: { asc: lastUpdate }, first: 10) { + projectName + } + tickets { + connectedGitIssue { ... } + } + velocityMeasure + gitDetails { ... } + repositories { ... } + } +} +``` + +--- diff --git a/wiki/content/graphql/custom/graphqlpm.md b/wiki/content/graphql/custom/graphqlpm.md new file mode 100644 index 00000000000..a488508ac64 --- /dev/null +++ b/wiki/content/graphql/custom/graphqlpm.md @@ -0,0 +1,104 @@ ++++ +title = "Custom DQL" +[menu.main] + parent = "custom" + weight = 6 ++++ + +At present, it is an experimental feature in master. You can specify the DQL (aka GraphQL+-) query you want to execute +while running a custom GraphQL query, and Dgraph's GraphQL API will execute that for you. + +It helps to build logic that you can't do with the current GraphQL CRUD API. + +For example, lets say you had following schema: +```graphql +type Tweets { + id: ID! + text: String! @search(by: [fulltext]) + author: User + timestamp: DateTime! @search +} +type User { + screen_name: String! @id + followers: Int @search + tweets: [Tweets] @hasInverse(field: author) +} +``` + +and you wanted to query tweets containing some particular text sorted by the number of followers their author has. Then, +this is not possible with the automatically generated CRUD API. Similarly, let's say you have a table sort of UI +component in your application which displays only a user's name and the number of tweets done by that user. Doing this +with the auto-generated CRUD API would require you to fetch unnecessary data at client side, and then employ client side +logic to find the count. Instead, all this could simply be achieved by specifying a DQL query for such custom use-cases. + +So, you would need to modify your schema like this: +```graphql +type Tweets { + id: ID! + text: String! @search(by: [fulltext]) + author: User + timestamp: DateTime! @search +} +type User { + screen_name: String! @id + followers: Int @search + tweets: [Tweets] @hasInverse(field: author) +} +type UserTweetCount @remote { + screen_name: String + tweetCount: Int +} + +type Query { + queryTweetsSortedByAuthorFollowers(search: String!): [Tweets] @custom(dql: """ + query q($search: string) { + var(func: type(Tweets)) @filter(anyoftext(Tweets.text, $search)) { + Tweets.author { + followers as User.followers + } + authorFollowerCount as sum(val(followers)) + } + queryTweetsSortedByAuthorFollowers(func: uid(authorFollowerCount), orderdesc: val(authorFollowerCount)) { + id: uid + text: Tweets.text + author: Tweets.author { + screen_name: User.screen_name + followers: User.followers + } + timestamp: Tweets.timestamp + } + } + """) + + queryUserTweetCounts: [UserTweetCount] @custom(dql: """ + query { + queryUserTweetCounts(func: type(User)) { + screen_name: User.screen_name + tweetCount: count(User.tweets) + } + } + """) +} + +``` + +Now, if you run following query, it would fetch you the tweets containing "GraphQL" in their text, sorted by the number +of followers their author has: +```graphql +query { + queryTweetsSortedByAuthorFollowers(search: "GraphQL") { + text + } +} +``` + +There are following points to note while specifying the DQL query for such custom resolvers: + +* The name of the DQL query that you want to map to the GraphQL response, should be same as the name of the GraphQL query. +* You must use proper aliases inside DQL queries to map them to the GraphQL response. +* If you are using variables in DQL queries, their names should be same as the name of the arguments for the GrapqhQL query. +* For variables, only scalar GraphQL arguments like Boolean, Int, Float etc are allowed. Lists and Object types are not allowed to be used as variables with DQL queries. +* You would be able to query only those many levels with GraphQL which you have mapped with the DQL query. For instance, in the first custom query above, we haven't mapped an author's tweets to GraphQL alias, so, we won't be able to fetch author's tweets using that query. +* If the custom GraphQL query returns an interface, and you want to use `__typename` in GraphQL query, then you should add `dgraph.type` as a field in DQL query without any alias. This is not required for types, only for interfaces. + +--- \ No newline at end of file diff --git a/wiki/content/graphql/custom/mutation.md b/wiki/content/graphql/custom/mutation.md new file mode 100644 index 00000000000..40455f184f7 --- /dev/null +++ b/wiki/content/graphql/custom/mutation.md @@ -0,0 +1,47 @@ ++++ +title = "Custom Mutations" +[menu.main] + parent = "custom" + weight = 4 ++++ + +Let's say we have an application about authors and posts. Logged in authors can add posts, but we want to do some input validation and add extra value when a post is added. The key types might be as follows. + +```graphql +type Author { ... } + +type Post { + id: ID: + title: String + text: String + datePublished: DateTime + author: Author + ... +} +``` + +Dgraph generates an `addPost` mutation from those types, but we want to do something extra. We don't want the `author` to come in with the mutation, that should get filled in from the JWT of the logged in user. Also, the `datePublished` shouldn't be in the input; it should be set as the current time at point of mutation. Maybe we also have some community guidelines about what might constitute an offensive `title` or `text` in a post. Maybe users can only post if they have enough community credit. + +We'll need custom code to do all that, so we can write a custom function that takes in only the title and text of the new post. Internally, it can check that the title and text satisfy the guidelines and that this user has enough credit to make a post. If those checks pass, it then builds a full post object by adding the current time as the `datePublished` and adding the `author` from the JWT information it gets from the forward header. It can then call the `addPost` mutation constructed by Dgraph to add the post into Dgraph and returns the resulting post as its GraphQL output. + +So as well as the types above, we need a custom mutation: + +```graphql +type Mutation { + newPost(title: String!, text: String): Post @custom(http:{ + url: "https://my.api.com/addPost" + method: "POST", + body: "{ postText: $text, postTitle: $title }" + forwardHeaders: ["AuthHdr"] + }) +} +``` + +## Learn more + +Find out more about how to turn off generated mutations and protecting mutations with authorization rules at: + +* [Remote Types - Turning off Generated Mutations with `@remote` Directive](/graphql/custom/directive) +* [Securing Mutations with the `@auth` Directive](/graphql/authorization/mutations) + +--- diff --git a/wiki/content/graphql/custom/query.md b/wiki/content/graphql/custom/query.md new file mode 100644 index 00000000000..8cb012bbbe0 --- /dev/null +++ b/wiki/content/graphql/custom/query.md @@ -0,0 +1,69 @@ ++++ +title = "Custom Queries" +[menu.main] + parent = "custom" + weight = 3 ++++ + +Let's say we want to integrate our app with an existing external REST API. There's a few things we need to know: + +* the URL of the API, the path and any parameters required +* the shape of the resulting JSON data +* the method (GET, POST, etc.), and +* what authorization we need to pass to the external endpoint + +The custom query can take any number of scalar arguments and use those to construct the path, parameters and body (we'll see an example of that in the custom mutation section) of the request that gets sent to the remote endpoint. + +In an app, you'd deploy an endpoint that does some custom work and returns data that's used in your UI, or you'd wrap some logic or call around an existing endpoint. So that we can walk through a whole example, let's use the Twitter API. + +To integrate a call that returns the data of Twitter user with our app, all we need to do is add the expected result type `TwitterUser` and set up a custom query: + +```graphql +type TwitterUser @remote { + id: ID! + name: String + screen_name: String + location: String + description: String + followers_count: Int + ... +} + +type Query{ + getCustomTwitterUser(name: String!): TwitterUser @custom(http:{ + url: "https://api.twitter.com/1.1/users/show.json?screen_name=$name" + method: "GET", + forwardHeaders: ["Authorization"] + }) +} +``` + +Dgraph will then be able to accept a GraphQL query like + +```graphql +query { + getCustomTwitterUser(name: "dgraphlabs") { + location + description + followers_count + } +} +``` + +construct a HTTP GET request to `https://api.twitter.com/1.1/users/show.json?screen_name=dgraphlabs`, attach header `Authorization` from the incoming GraphQL request to the outgoing HTTP, and make the call and return a GraphQL result. + +The result JSON of the actual HTTP call will contain the whole object from the REST endpoint (you can see how much is in the Twitter user object [here](https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/user-object)). But, the GraphQL query only asked for some of that, so Dgraph filters out any returned values that weren't asked for in the GraphQL query and builds a valid GraphQL response to the query and returns GraphQL. + +```json +{ + "data": { + "getCustomTwitterUser": { "location": ..., "description": ..., "followers_count": ... } + } +} +``` + +Your version of the remote type doesn't have to be equal to the remote type. For example, if you don't want to allow users to query the full Twitter user, you include in the type definition only the fields that can be queried. + +All the usual options for custom queries are allowed; for example, you can have multiple queries in a single GraphQL request and a mix of custom and Dgraph generated queries, you can get the result compressed by setting `Accept-Encoding` to `gzip`, etc. + +--- diff --git a/wiki/content/graphql/dgraph/index.md b/wiki/content/graphql/dgraph/index.md new file mode 100644 index 00000000000..69c373b8883 --- /dev/null +++ b/wiki/content/graphql/dgraph/index.md @@ -0,0 +1,111 @@ ++++ +title = "GraphQL on Existing Dgraph" +[menu.main] + url = "/graphql/dgraph/" + identifier = "dgraph" + parent = "graphql" + weight = 13 ++++ + + + +How to use GraphQL on an existing Dgraph instance. + +If you have an existing Dgraph instance and want to also expose GraphQL, you need to add a GraphQL schema that maps to your Dgraph schema. You don't need to expose your entire Dgraph schema as GraphQL, but do note that adding a GraphQL schema can alter the Dgraph schema. + +Dgraph also allows type and edge names that aren't valid in GraphQL, so, often, you'll need to expose valid GraphQL names. Dgraph admits special characters and even different languages (see [here](https://docs.dgraph.io/query-language/#predicate-name-rules)), while the GraphQL Spec requires that type and field (predicate) names are generated from `/[_A-Za-z][_0-9A-Za-z]*/`. + +# Mapping GraphQL to a Dgraph schema + +By default, Dgraph generates a new predicate for each field in a GraphQL type. The name of the generated predicate is composed of the type name followed by a dot `.` and ending with the field name. Therefore, two different types with fields of the same name will turn out to be different Dgraph predicates and can have different indexes. For example, the types: + +```graphql +type Person { + name: String @search(by: [hash]) + age: Int +} + +type Movie { + name: String @search(by: [term]) +} +``` + +generate a Dgraph schema like: + +```graphql +type Person { + Person.name + Person.age +} + +type Movie { + Movie.name +} + +Person.name: string @index(hash) . +Person.age: int . +Movie.name: string @index(term) . +``` + +This behavior can be customized with the `@dgraph` directive. + +* `type T @dgraph(type: "DgraphType")` controls what Dgraph type is used for a GraphQL type. +* `field: SomeType @dgraph(pred: "DgraphPredicate")` controls what Dgraph predicate is mapped to a GraphQL field. + +For example, if you have existing types that don't match GraphQL requirements, you can create a schema like the following. + +```graphql +type Person @dgraph(type: "Human-Person") { + name: String @search(by: [hash]) @dgraph(pred: "name") + age: Int +} + +type Movie @dgraph(type: "film") { + name: String @search(by: [term]) @dgraph(pred: "film.name") +} +``` + +Which maps to the Dgraph schema: + +```graphql +type Human-Person { + name + Person.age +} + +type film { + film.name +} + +name string @index(hash) . +Person.age: int . +film.name string @index(term) . +``` + +You might also have the situation where you have used `name` for both movie names and people's names. In this case you can map fields in two different GraphQL types to the one Dgraph predicate. + +```graphql +type Person { + name: String @dgraph(pred: "name") + ... +} + +type Movie { + name: String @dgraph(pred: "name") + ... +} +``` + +*Note: the current behavior requires that when two fields are mapped to the same Dgraph predicate both should have the same `@search` directive. This is likely to change in a future release where the underlying Dgraph indexes will be the union of the `@search` directives, while the generated GraphQL API will expose only the search given for the particular field. Allowing, for example, dgraph predicate name to have `term` and `hash` indexes, but exposing only term search for GraphQL movies and hash search for GraphQL people.* + +# Roadmap + +Be careful with mapping to an existing Dgraph instance. Updating the GraphQL schema updates the underlying Dgraph schema. We understand that exposing a GraphQL API on an existing Dgraph instance is a delicate process and we plan on adding multiple checks to ensure the validity of schema changes to avoid issues caused by detectable mistakes. + +Future features are likely to include: + +* Generating a first pass GraphQL schema from an existing dgraph schema. +* A way to show what schema diff will happen when you apply a new GraphQL schema. +* Better handling of `@dgraph` with `@search` + +We look forward to you letting us know what features you'd like, so please join us on [discuss](https://discuss.dgraph.io/) or [GitHub](https://github.com/dgraph-io/dgraph). diff --git a/wiki/content/graphql/directives/index.md b/wiki/content/graphql/directives/index.md new file mode 100644 index 00000000000..618f18d9d72 --- /dev/null +++ b/wiki/content/graphql/directives/index.md @@ -0,0 +1,73 @@ ++++ +title = "Index of Directives" +[menu.main] + url = "/graphql/directives/" + name = "Directives" + identifier = "directives" + parent = "graphql" + weight = 11 ++++ + +The list of all directives supported by Dgraph. + +### @hasInverse + +`@hasInverse` is used to setup up two way edges such that adding a edge in +one direction automically adds the one in the inverse direction. + +Reference: [Linking nodes in the graph](/graphql/schema/graph-links) + +### @search + +`@search` allows you to perform filtering on a field while querying for nodes. + +Reference: [Search](/graphql/schema/search) + +### @dgraph + +`@dgraph` directive tells us how to map fields within a type to existing predicates inside Dgraph. + +Reference: [GraphQL on Existing Dgraph](/graphql/dgraph/) + + +### @id + +`@id` directive is used to annotate a field which represents a unique identifier coming from outside + of Dgraph. + +Reference: [Identity](/graphql/schema/ids) + +### @withSubscription + +`@withSubscription` directive when applied on a type, generates subsciption queries for it. + +Reference: [Subscriptions](/graphql/subscriptions) + +### @secret + +TODO - After adding docs for password type. + +### @auth + +`@auth` allows you to define how to apply authorization rules on the queries/mutation for a type. + +Reference: [Auth directive](/graphql/authorization/directive) + +### @custom + +`@custom` directive is used to define custom queries, mutations and fields. + +Reference: [Custom directive](/graphql/custom/directive) + +### @remote + +`@remote` directive is used to annotate types for which data is not stored in Dgraph. These types +are typically used with custom queries and mutations. + +Reference: [Remote directive](/graphql/custom/directive) + +### @cascade + +`@cascade` allows you to filter out certain nodes within a query. + +Reference: [Cascade](/graphql/queries/cascade) \ No newline at end of file diff --git a/wiki/content/graphql/how-dgraph-graphql-works/index.md b/wiki/content/graphql/how-dgraph-graphql-works/index.md new file mode 100644 index 00000000000..9987587e18a --- /dev/null +++ b/wiki/content/graphql/how-dgraph-graphql-works/index.md @@ -0,0 +1,79 @@ ++++ +title = "How GraphQL works within Dgraph" +[menu.main] + url = "/graphql/how-dgraph-graphql-works/" + name = "How GraphQL works within Dgraph" + identifier = "how-dgraph-graphql-works" + parent = "graphql" + weight = 2 ++++ + +Dgraph is a GraphQL database. That means, with Dgraph, you design your application in GraphQL, you iterate on your app in GraphQL and, when you need it, you scale with GraphQL. + +You design a set of GraphQL types that describes your requirements. Dgraph takes those types, prepares graph storage for them and generates a GraphQL API with queries and mutations. + +You design a graph, store a graph and query a graph. You think and design in terms of the graph that your app is based around. + +Let's look at how that might work a simple Twitter clone. + +## The app building process + +You'll have an idea for your app, maybe you've sketched out the basic UI, or maybe you've worked out the basic things in your app and their relationships. From that, you can derive a first version of your schema. + +```graphql +type User { + username: String! @id + tweets: [Tweet] +} + +type Tweet { + text: String! +} +``` + +Load that into Dgraph, and you'll have a working GraphQL API. You can start doing example queries and mutations with a tool like GraphQL Playground or Insomnia, you can even jump straight in and start building a UI with, say, Apollo Client. That's how quickly you can get started. + +Soon, you'll need to iterate while you are developing, or need to produce the next version of your idea. Either way, Dgraph makes it easy to iterate on your app. Add extra fields, add search, and Dgraph adjusts. + +```graphql +type User { + username: String! @id + tweets: [Tweet] +} + +type Tweet { + text: String! @search(by: [fulltext]) + datePosted: DateTime +} +``` + +You can even do data migrations in GraphQL, so you never have to think about anything other than GraphQL. + +Eventually, you'll need custom business logic and bespoke code to enhance your GraphQL server. You can write that code however works best for your app and then integrate it directly into your GraphQL schema. + +```graphql +type User { + ... +} + +type Tweet { + ... + myCustomField @custom(...) +} + +type Query { + MyCustomQuery @custom(...) +} +``` + +Again, Dgraph adjusts, and you keep working on your app, not on translating another data format into a graph. + +## GraphQL, Dgraph and Graphs + +You might be familiar with GraphQL types, fields and resolvers. Perhaps you've written an app that adds GraphQL over a REST endpoint or maybe over a relational database. If so, you know how GraphQL sits over those sources and issues many queries to translate the REST/relational data into something that looks like a graph. + +There's a cognitive jump in that process because your app is about a graph, but you've got to design a relational schema and work out how that translates as a graph. You'll be thinking about the app in terms of the graph, but have to mentally translate back and forth between the relational and graph models. There are engineering challenges around the translation as well as the efficiency of the queries. + +There's none of that with Dgraph. + +Dgraph GraphQL is part of Dgraph, which stores a graph - it's a database of nodes and edges. So it's efficient to store, query and traverse as a graph. Your data will get stored just like you design it in the schema, and the queries are a single graph query that does just what the GraphQL query says. diff --git a/wiki/content/graphql/mutations/_index.md b/wiki/content/graphql/mutations/_index.md new file mode 100644 index 00000000000..64326a2e864 --- /dev/null +++ b/wiki/content/graphql/mutations/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Mutations" +[menu.main] + url = "/graphql/mutations/" + identifier = "graphql-mutations" + parent = "graphql" + weight = 7 ++++ diff --git a/wiki/content/graphql/mutations/add.md b/wiki/content/graphql/mutations/add.md new file mode 100644 index 00000000000..c743bd6b432 --- /dev/null +++ b/wiki/content/graphql/mutations/add.md @@ -0,0 +1,81 @@ ++++ +title = "Add Mutations" +[menu.main] + parent = "graphql-mutations" + name = "Add" + weight = 2 ++++ + +Add Mutations allows you to add new objects of a particular type. + +We use the following schema to demonstrate some examples. + +**Schema**: +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +Dgraph automatically generates input and return type in the schema for the add mutation. +```graphql +addPost(input: [AddPostInput!]!): AddPostPayload + +input AddPostInput { + title: String! + text: String + datePublished: DateTime +} + +type AddPostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + numUids: Int +} +``` + +**Example**: Add mutation on single type with embedded value +```graphql +mutation { + addAuthor(input: [{ name: "A.N. Author", posts: []}]) { + author { + id + name + } + } +} +``` + +**Example**: Add mutation on single type using variables +```graphql +mutation addAuthor($author: [AddAuthorInput!]!) { + addAuthor(input: $author) { + author { + id + name + } + } +} +``` +Variables: +```json +{ "auth": + { "name": "A.N. Author", + "dob": "2000-01-01", + "posts": [] + } +} +``` + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/blob/master/graphql/resolve/add_mutation_test.yaml) for more examples. diff --git a/wiki/content/graphql/mutations/deep.md b/wiki/content/graphql/mutations/deep.md new file mode 100644 index 00000000000..b9414782e6d --- /dev/null +++ b/wiki/content/graphql/mutations/deep.md @@ -0,0 +1,89 @@ ++++ +title = "Deep Mutations" +[menu.main] + parent = "graphql-mutations" + name = "Deep" + weight = 5 ++++ + +Mutations also allows to perform deep mutation at multiple levels. + +We use the following schema to demonstrate some examples. + +## **Schema**: +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +### **Example**: Deep Deep mutation using variables +```graphql +mutation DeepAuthor($author: DeepAuthorInput!) { + DeepAuthor(input: [$author]) { + author { + id + name + post { + title + text + } + } + } +} +``` +Variables: +```json +{ "author": + { "name": "A.N. Author", + "dob": "2000-01-01", + "posts": [ + { + "title": "New post", + "text": "A really new post" + } + ] + } +} +``` + +### **Example**: Deep update mutation using variables +```graphql +mutation updateAuthor($patch: UpdateAuthorInput!) { + updateAuthor(input: $patch) { + author { + id + post { + title + text + } + } + } +} +``` +Variables: +```json +{ "patch": + { "filter": { + "id": ["0x123"] + }, + "set": { + "posts": [ { + "postID": "0x456", + "title": "A new title", + "text": "Some edited text" + } ] + } + } +} +``` diff --git a/wiki/content/graphql/mutations/delete.md b/wiki/content/graphql/mutations/delete.md new file mode 100644 index 00000000000..3894215038a --- /dev/null +++ b/wiki/content/graphql/mutations/delete.md @@ -0,0 +1,64 @@ ++++ +title = "Delete Mutations" +[menu.main] + parent = "graphql-mutations" + name = "Delete" + identifier = "graphql-delete" + weight = 4 ++++ + +Delete Mutations allows you to delete objects of a particular type. + +We use the following schema to demonstrate some examples. + +**Schema**: +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +Dgraph automatically generates input and return type in the schema for the delete mutation. +Delete Mutations takes filter as an input to select specific objects and returns the state of the objects before deletion. +```graphql +deleteAuthor(filter: AuthorFilter!): DeleteAuthorPayload + +type DeleteAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + msg: String + numUids: Int +} +``` + +**Example**: Delete mutation using variables +```graphql +mutation deleteAuthor($filter: AuthorFilter!) { + deleteAuthor(filter: $filter) { + msg + author { + name + dob + } + } +} +``` +Variables: +```json +{ "filter": + { "name": { "eq": "A.N. Author" } } +} +``` + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/blob/master/graphql/resolve/delete_mutation_test.yaml) for more examples. diff --git a/wiki/content/graphql/mutations/mutations-overview.md b/wiki/content/graphql/mutations/mutations-overview.md new file mode 100644 index 00000000000..29c0fdde114 --- /dev/null +++ b/wiki/content/graphql/mutations/mutations-overview.md @@ -0,0 +1,147 @@ ++++ +title = "Overview" +[menu.main] + parent = "graphql-mutations" + identifier = "mutations-overview" + weight = 1 ++++ + +Mutation allows us to modify server-side data, and it also returns an object based on the operation performed. It can be used to insert, update, or delete data. Dgraph automatically generates GraphQL mutation for each type that you define in your schema. The mutation field returns an object type that allows you to query for nested fields. This can be useful for fetching an object's new state after an add/update or get the old state of an object before a delete. + +**Example** + +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +The following mutations would be generated from the above schema. + +```graphql +type Mutation { + addAuthor(input: [AddAuthorInput!]!): AddAuthorPayload + updateAuthor(input: UpdateAuthorInput!): UpdateAuthorPayload + deleteAuthor(filter: AuthorFilter!): DeleteAuthorPayload + addPost(input: [AddPostInput!]!): AddPostPayload + updatePost(input: UpdatePostInput!): UpdatePostPayload + deletePost(filter: PostFilter!): DeletePostPayload +} + +type AddAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + numUids: Int +} + +type AddPostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + numUids: Int +} + +type DeleteAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + msg: String + numUids: Int +} + +type DeletePostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + msg: String + numUids: Int +} + +type UpdateAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + numUids: Int +} + +type UpdatePostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + numUids: Int +} +``` + +## Input objects +Mutations require input data, such as the data, to create a new object or an object's ID to delete. Dgraph auto-generates the input object type for every type in the schema. + +```graphql +input AddAuthorInput { + name: String! + dob: DateTime + posts: [PostRef] +} + +mutation { + addAuthor( + input: { + name: "A.N. Author", + lastName: "2000-01-01", + } + ) + { + ... + } +} +``` + +## Return fields +Each mutation provides a set of fields that can be returned in the response. Dgraph auto-generates the return payload object type for every type in the schema. + +```graphql +type AddAuthorPayload { + author(filter: AuthorFilter, order: AuthorOrder, first: Int, offset: Int): [Author] + numUids: Int +} +``` + +## Multiple fields in mutations +A mutation can contain multiple fields, just like a query. While query fields are executed in parallel, mutation fields run in series, one after the other. This means that if we send two `updateAuthor` mutations in one request, the first is guaranteed to finish before the second begins. This ensures that we don't end up with a race condition with ourselves. If one of the mutations is aborted due error like transaction conflict, we continue performing the next mutations. + +**Example**: Mutation on multiple types +```graphql +mutation ($post: AddPostInput!, $author: AddAuthorInput!) { + addAuthor(input: [$author]) { + author { + name + } + } + addPost(input: [$post]) { + post { + postID + title + text + } + } +} +``` +Variables: +```json +{ + "author": { + "name": "A.N. Author", + "dob": "2000-01-01", + "posts": [] + }, + "post": { + "title": "Exciting post", + "text": "A really good post", + "author": { + "name": "A.N. Author" + } + } +} +``` + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/tree/master/graphql/schema/testdata/schemagen) for more examples. diff --git a/wiki/content/graphql/mutations/update.md b/wiki/content/graphql/mutations/update.md new file mode 100644 index 00000000000..bae512f5048 --- /dev/null +++ b/wiki/content/graphql/mutations/update.md @@ -0,0 +1,98 @@ ++++ +title = "Update Mutations" +[menu.main] + parent = "graphql-mutations" + name = "Update" + weight = 3 ++++ + +Update Mutations allows you to update existing objects of a particular type. It allows to filter nodes and, set and remove any field belonging to a type. + +We use the following schema to demonstrate some examples. + +**Schema**: +```graphql +type Author { + id: ID! + name: String! @search(by: [hash]) + dob: DateTime + posts: [Post] +} + +type Post { + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext, term]) + datePublished: DateTime +} +``` + +Dgraph automatically generates input and return type in the schema for the update mutation. Update mutation takes filter as an input to select specific objects. You can specify set and remove operation on fields belonging to the filtered objects. It returns the state of the objects after updation. +```graphql +updatePost(input: UpdatePostInput!): UpdatePostPayload + +input UpdatePostInput { + filter: PostFilter! + set: PostPatch + remove: PostPatch +} + +type UpdatePostPayload { + post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] + numUids: Int +} +``` + +**Example**: Update set mutation using variables +```graphql +mutation updatePost($patch: UpdatePostInput!) { + updatePost(input: $patch) { + post { + postID + title + text + } + } +} +``` +Variables: +```json +{ "patch": + { "filter": { + "postID": ["0x123", "0x124"] + }, + "set": { + "text": "updated text" + } + } +} +``` + +**Example**: Update remove mutation using variables +```graphql +mutation updatePost($patch: UpdatePostInput!) { + updatePost(input: $patch) { + post { + postID + title + text + } + } +} +``` +Variables: +```json +{ "patch": + { "filter": { + "postID": ["0x123", "0x124"] + }, + "remove": { + "text": "delete this text" + } + } +} +``` + +## Examples + +You can refer to the following [link](https://github.com/dgraph-io/dgraph/blob/master/graphql/resolve/update_mutation_test.yaml) for more examples. diff --git a/wiki/content/graphql/overview/index.md b/wiki/content/graphql/overview/index.md new file mode 100644 index 00000000000..403a176136d --- /dev/null +++ b/wiki/content/graphql/overview/index.md @@ -0,0 +1,74 @@ ++++ +title = "Overview" +[menu.main] + url = "/graphql/overview/" + name = "Overview" + identifier = "graphql-overview" + parent = "graphql" + weight = 1 ++++ + +**Welcome to the official GraphQL documentation for Dgraph.** + +Designed from the ground up to be run in production, Dgraph is the native GraphQL database with a graph backend. It is open-source, scalable, distributed, highly available and lightning fast. + +* These docs tell you all the details. If you are looking for a walk through tutorial, then head over to our [tutorials section](/graphql/todo-app-tutorial/todo-overview). + +Dgraph gives you GraphQL. You're always working with GraphQL, not a translation layer. When you build an app with Dgraph, Dgraph is your GraphQL database. + +## Exploring the docs + +* How it Works - Once you've got yourself started with [tutorials](/graphql/todo-app-tutorial/todo-overview), you might need to review [how it works](/graphql/how-dgraph-graphql-works). +* [Schema](/graphql/schema/schema-overview) - You'll need the schema reference to find out about all the options of what can be in your schema. +* [The API](/graphql/api/api-overview) - The API section tells you about how the GraphQL API is served and how you can access it. +* [Queries](/graphql/queries/queries-overview) - Everything you need to know about writing GraphQL queries. +* [Mutations](/graphql/mutations/mutations-overview) - Everything you need to know about writing GraphQL mutations with Dgraph. +* [Subscriptions](/graphql/subscriptions) - GraphQL subscriptions help you make your APP more responsive or, for example, add live feeds. Dgraph can generate subscriptions for you. +* [Custom Logic](/graphql/custom/custom-overview) - Dgraph's auto generated GraphQL API is fantastic, but as your app gets more complicated, you'll need to add custom business logic to your API. +* [Authorization](/graphql/authorization/authorization-overview) - Find out how Dgraph can add automated authorization to your GraphQL API. +* [Local Administration](/graphql/admin) - Once you're up and running you might also need to know a few details about administering your Dgraph instance if you are running locally. +* [Slash GraphQL](/slash-graphql/admin/overview) - If you are using hosted Dgraph on Slash GraphQL, then head over here to learn about administering your backend. + +## Contribute + +
+
+
+
+
+ +

+ Get started with contributing fixes and enhancements to Dgraph and related software. +

+
+
+
+
+
+ +## Our Community + +**Dgraph is made better every day by the growing community and the contributors all over the world.** + +
+
+
+
+
+ +

+ Discuss Dgraph on the official community. +

+
+
+
+
+
\ No newline at end of file diff --git a/wiki/content/graphql/queries/_index.md b/wiki/content/graphql/queries/_index.md new file mode 100644 index 00000000000..c19923f4019 --- /dev/null +++ b/wiki/content/graphql/queries/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Queries" +[menu.main] + url = "/graphql/queries/" + identifier = "graphql-queries" + parent = "graphql" + weight = 6 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/queries/and-or-not.md b/wiki/content/graphql/queries/and-or-not.md new file mode 100644 index 00000000000..83abe4f7a13 --- /dev/null +++ b/wiki/content/graphql/queries/and-or-not.md @@ -0,0 +1,53 @@ ++++ +title = "And, Or and Not" +[menu.main] + parent = "graphql-queries" + name = "And, Or and Not" + weight = 3 ++++ + +Every search filter contains `and`, `or` and `not`. + +GraphQL's syntax is used to write these infix style, so "a and b" is written `a, and: { b }`, and "a or b or c" is written `a, or: { b, or: c }`. Not is written prefix. + +The posts that do not have "GraphQL" in the title. + +```graphql +queryPost(filter: { not: { title: { allofterms: "GraphQL"} } } ) { ... } +``` + +The posts that have "GraphQL" or "Dgraph" in the title. + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL"}, + or: { title: { allofterms: "Dgraph" } } + } ) { ... } +``` + +The posts that have "GraphQL" and "Dgraph" in the title. + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL"}, + and: { title: { allofterms: "Dgraph" } } + } ) { ... } +``` + +The and is implicit for a single filter object, if the fields don't overlap. For example, above the `and` is required because `title` is in both filters, where as below, `and` is not required. + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL" }, + datePublished: { ge: "2020-06-15" } + } ) { ... } +``` + +The posts that have "GraphQL" in the title, or have the tag "GraphQL" and mention "Dgraph" in the title + +```graphql +queryPost(filter: { + title: { allofterms: "GraphQL"}, + or: { title: { allofterms: "Dgraph" }, tags: { eg: "GraphQL" } } + } ) { ... } +``` \ No newline at end of file diff --git a/wiki/content/graphql/queries/cascade.md b/wiki/content/graphql/queries/cascade.md new file mode 100644 index 00000000000..49b2c0e0990 --- /dev/null +++ b/wiki/content/graphql/queries/cascade.md @@ -0,0 +1,43 @@ ++++ +title = "Cascade" +[menu.main] + parent = "graphql-queries" + name = "Cascade" + weight = 5 ++++ + +`@cascade` is available as a directive which can be applied on fields. With the @cascade +directive, nodes that don’t have all fields specified in the query are removed. +This can be useful in cases where some filter was applied and some nodes might not +have all listed fields. + +For example, the query below would only return the authors which have both reputation +and posts and where posts have text. Note that `@cascade` trickles down so it would +automatically be applied at the `posts` level as well if its applied at the `queryAuthor` +level. + +```graphql +{ + queryAuthor @cascade { + reputation + posts { + text + } + } +} +``` + +`@cascade` can also be used at nested levels, so the query below would return all authors +but only those posts which have both `text` and `id`. + +```graphql +{ + queryAuthor { + reputation + posts @cascade { + id + text + } + } +} +``` \ No newline at end of file diff --git a/wiki/content/graphql/queries/order-page.md b/wiki/content/graphql/queries/order-page.md new file mode 100644 index 00000000000..075cdf4d1c8 --- /dev/null +++ b/wiki/content/graphql/queries/order-page.md @@ -0,0 +1,30 @@ ++++ +title = "Order and Pagination" +[menu.main] + parent = "graphql-queries" + name = "Order and Pagination" + weight = 4 ++++ + +Every type with fields whose types can be ordered (`Int`, `Float`, `String`, `DateTime`) gets +ordering built into the query and any list fields of that type. Every query and list field +gets pagination with `first` and `offset` and ordering with `order` parameter. + +For example, find the most recent 5 posts. + +```graphql +queryPost(order: { desc: datePublished }, first: 5) { ... } +``` + +Skip the first five recent posts and then get the next 10. + +```graphql +queryPost(order: { desc: datePublished }, offset: 5, first: 10) { ... } +``` + +It's also possible to give multiple orders. For example, sort by date and within each +date order the posts by number of likes. + +```graphql +queryPost(order: { desc: datePublished, then: { desc: numLikes } }, first: 5) { ... } +``` \ No newline at end of file diff --git a/wiki/content/graphql/queries/queries-overview.md b/wiki/content/graphql/queries/queries-overview.md new file mode 100644 index 00000000000..9efc382c1e3 --- /dev/null +++ b/wiki/content/graphql/queries/queries-overview.md @@ -0,0 +1,46 @@ ++++ +title = "Overview" +[menu.main] + parent = "graphql-queries" + identifier = "queries-overview" + weight = 1 ++++ + +How to use queries to fetch data from Dgraph. + +Dgraph automatically generates GraphQL queries for each type that you define in +your schema. There are two types of of queries generated for each type. + +Example + +```graphql +type Post { + id: ID! + title: String! @search + text: String + score: Float @search + completed: Boolean @search + datePublished: DateTime @search(by: [year]) + author: Author! +} + +type Author { + id: ID! + name: String! @search + posts: [Post!] + friends: [Author] +} +``` + +With the above schema, there would be two queries generated for Post and two +for Author. Here are the queries that are generated for the Post type: + +```graphql +getPost(postID: ID!): Post +queryPost(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] +``` + +The first query allows you to fetch a post and its related fields given an ID. +The second query allows you to fetch a list of posts based on some filters, sorting and +pagination parameters. You can look at all the queries that are generated by using any +GraphQL client such as Insomnia or GraphQL playground. \ No newline at end of file diff --git a/wiki/content/graphql/queries/search-filtering.md b/wiki/content/graphql/queries/search-filtering.md new file mode 100644 index 00000000000..5b6b8e8f517 --- /dev/null +++ b/wiki/content/graphql/queries/search-filtering.md @@ -0,0 +1,173 @@ ++++ +title = "Search and Filtering" +[menu.main] + parent = "graphql-queries" + name = "Search and Filtering" + weight = 2 ++++ + +Queries generated for a GraphQL type allow you to generate a single of list of +objects for a type. + +### Get a single object + +Fetch the title, text and datePublished for a post with id `0x1`. + +```graphql +query { + getPost(id: "0x1") { + title + text + datePublished + } +} +``` + +Fetching nested linked objects, while using get queries is also easy. This is how +you would fetch the authors for a post and their friends. + +```graphql +query { + getPost(id: "0x1") { + id + title + text + datePublished + author { + name + friends { + name + } + } + } +} +``` + +While fetching nested linked objects, you can also apply a filter on them. + +Example - Fetching author with id 0x1 and their posts about GraphQL. + +```graphql +query { + getAuthor(id: "0x1") { + name + posts(filter: { + title: { + allofterms: "GraphQL" + } + }) { + title + text + datePublished + } + } +} +``` + +If your type has a field with `@id` directive on it, you can also fetch objects using that. + +Example: To fetch a user's name and age by userID which has @id directive. + +Schema + +```graphql +type User { + userID: String! @id + name: String! + age: String +} +``` + +Query + +```graphql +query { + getUser(userID: "0x2") { + name + age + } +} +``` + +### Query list of objects + +Fetch the title, text and and datePublished for all the posts. + +```graphql +query { + queryPost { + id + title + text + datePublished + } +} +``` + +Fetching a list of posts by their ids. + +```graphql +query { + queryPost(filter: { + id: ["0x1", "0x2", "0x3", "0x4"], + }) { + id + title + text + datePublished + } +} +``` + +You also filter the posts by different fields in the Post type which have a +`@search` directive on them. To only fetch posts which `GraphQL` in their title +and have a `score > 100`, you can run the following query. + +```graphql +query { + queryPost(filter: { + title: { + anyofterms: "GraphQL" + }, + and: { + score: { + gt: 100 + } + } + }) { + id + title + text + datePublished + } +} +``` + +You can also filter nested objects while querying for a list of objects. + +Example - To fetch all the authors whose name have `Lee` in them and their`completed` posts +with score greater than 10. + +```graphql +query { + queryAuthor(filter: { + name: { + anyofterms: "Lee" + } + }) { + name + posts(filter: { + score: { + gt: 10 + }, + and: { + completed: true + } + }) { + title + text + datePublished + } + } +} +``` \ No newline at end of file diff --git a/wiki/content/graphql/queries/skip-include.md b/wiki/content/graphql/queries/skip-include.md new file mode 100644 index 00000000000..3d1609a075f --- /dev/null +++ b/wiki/content/graphql/queries/skip-include.md @@ -0,0 +1,63 @@ ++++ +title = "Skip and Include" +[menu.main] + parent = "graphql-queries" + name = "Skip and Include" + weight = 6 ++++ + +`@skip` and `@include` are directives which can be applied on a field while querying. +They allow you to skip or include a field based on the value of the `if` argument +that is passed to the directive. + +## @skip + +In the query below, we fetch posts and decide whether to fetch the title for them or not +based on the `skipTitle` GraphQL variable. + +GraphQL query + +```graphql +query ($skipTitle: Boolean!) { + queryPost { + id + title @skip(if: $skipTitle) + text + } +} +``` + +GraphQL variables +```json +{ + "skipTitle": true +} +``` + +## @include + +Similarly, the `@include` directive can be used to include a field based on the value of +the `if` argument. The query below would only include the authors for a post if `includeAuthor` +GraphQL variable has value true. + +GraphQL Query +```graphql +query ($includeAuthor: Boolean!) { + queryPost { + id + title + text + author @include(if: $includeAuthor) { + id + name + } + } +} +``` + +GraphQL variables +```json +{ + "includeAuthor": false +} +``` \ No newline at end of file diff --git a/wiki/content/graphql/quick-start/index.md b/wiki/content/graphql/quick-start/index.md new file mode 100644 index 00000000000..766ecb9c888 --- /dev/null +++ b/wiki/content/graphql/quick-start/index.md @@ -0,0 +1,259 @@ ++++ +title = "Quick Start" +[menu.main] + url = "/graphql/quick-start/" + name = "Quick Start" + identifier = "graphql-quick-start" + parent = "graphql" + weight = 1 ++++ + +Let's go from nothing to a running GraphQL API in just two steps. + +For GraphQL in Dgraph, you just concentrate on defining the schema of your graph and how you'd like to search that graph; Dgraph does the rest. You work only with GraphQL and, think in terms of the graph that matters for your app. + +This example is for an app about customers, products and reviews. That's a pretty simple graph, with just three types of objects, but it has some interesting connections for us to explore. + +Here's a schema of GraphQL types for that: + +```graphql +type Product { + productID: ID! + name: String @search(by: [term]) + reviews: [Review] @hasInverse(field: about) +} + +type Customer { + username: String! @id @search(by: [hash, regexp]) + reviews: [Review] @hasInverse(field: by) +} + +type Review { + id: ID! + about: Product! + by: Customer! + comment: String @search(by: [fulltext]) + rating: Int @search +} +``` + +With Dgraph you can turn that schema into a running GraphQL API in just two steps. + +## Step 1 - Start Dgraph GraphQL + +It's a one-liner to bring up Dgraph with GraphQL. *Note: The Dgraph standalone image is great for quick start and exploring, but it's not meant for production use. Once you want to build an App or persist your data for restarts, you'll need to review the [admin docs](/graphql/admin).* + +``` +docker run -it -p 8080:8080 dgraph/standalone:master +``` + +With that, GraphQL has started at localhost:8080/graphql, but it doesn't have a schema to serve yet. + +## Step 2 - Add a GraphQL Schema + +Dgraph will run your GraphQL API at `/graphql` and an admin interface at `/admin`. The `/admin` interface lets you add and update the GraphQL schema served at `/graphql`. The quickest way to reset the schema is just to post it to `/admin` with curl. + +Take the schema above, cut-and-paste it into a file called `schema.graphql` and run the following curl command. + +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +It'll post back the types it's currently serving a schema for, which should be the same as the input schema. + +That's it, now you've got a GraphQL API up and running. + +No, really, that's all; nothing else to do; it's there, serving GraphQL --- let's go use it. + +## GraphQL Mutations + +If you've followed the steps above, there's a GraphQL server up and running. You can access that GraphQL endpoint with any of the great GraphQL developer tools. Good choices include [GraphQL Playground](https://github.com/prisma-labs/graphql-playground), [Insomnia](https://insomnia.rest/), [GraphiQL](https://github.com/graphql/graphiql) and [Altair](https://github.com/imolorhe/altair). + +Fire one of those up and point it at `http://localhost:8080/graphql`. If you know lots about GraphQL, you might want to explore the schema, queries and mutations that were generated from the input. + +We'll begin by adding some products and an author. GraphQL can accept multiple mutations at a time, so it's one request. Neither the products nor the author will have any reviews yet, so all we need is the names. + +```graphql +mutation { + addProduct(input: [ + { name: "GraphQL on Dgraph"}, + { name: "Dgraph: The GraphQL Database"} + ]) { + product { + productID + name + } + } + addCustomer(input: [{ username: "Michael"}]) { + customer { + username + } + } +} +``` + +The GraphQL server will return a json response like: + +```json +{ + "data": { + "addProduct": { + "product": [ + { + "productID": "0x2", + "name": "GraphQL on Dgraph" + }, + { + "productID": "0x3", + "name": "Dgraph: The GraphQL Database" + } + ] + }, + "addCustomer": { + "customer": [ + { + "username": "Michael" + } + ] + } + }, + "extensions": { + "requestID": "b155867e-4241-4cfb-a564-802f2d3808a6" + } +} +``` + +And, of course, our author bought "GraphQL on Dgraph", loved it, and added a glowing review with the following mutation. + +Because the schema defined Customer with the field `username: String! @id`, the `username` field acts like an ID, so we can identify customers just with their names. Products, on the other hand, had `productID: ID!`, so they'll get an auto-generated ID. Your ID for the product might be different. Make sure you check the result of adding the products and use the right ID - it's no different to linking primary/foreign keys correctly in a relational DB. + +```graphql +mutation { + addReview(input: [{ + by: {username: "Michael"}, + about: { productID: "0x2"}, + comment: "Fantastic, easy to install, worked great. Best GraphQL server available", + rating: 10}]) + { + review { + comment + rating + by { username } + about { name } + } + } +} +``` + +This time, the mutation result queries for the author making the review and the product being reviewed, so it's gone deeper into the graph to get the result than just the mutation data. + +```json +{ + "data": { + "addReview": { + "review": [ + { + "comment": "Fantastic, easy to install, worked great. Best GraphQL server available", + "rating": 10, + "by": { + "username": "Michael" + }, + "about": { + "name": "GraphQL on Dgraph" + } + } + ] + } + }, + "extensions": { + "requestID": "11bc2841-8c19-45a6-bb31-7c37c9b027c9" + } +} +``` + +Already we have a running GraphQL API and can add data using any GraphQL tool. You could write a GraphQL/React app with a nice UI. It's GraphQL, so you can do anything GraphQL with your new server. + +Go ahead, add some more customers, products and reviews and then move on to querying data back out. + +## GraphQL Queries + +Mutations are one thing, but query is where GraphQL really shines. With GraphQL, you get just the data you want, in a format that's suitable for your app. + +With Dgraph, you get powerful graph search built into your GraphQL API. The schema for search is generated from the schema document that we started with and automatically added to the GraphQL API for you. + +Remember the definition of a review. + +```graphql +type Review { + ... + comment: String @search(by: [fulltext]) + ... +} +``` + +The directive `@search(by: [fulltext])` tells Dgraph we want to be able to search for comments with full-text search. That's Google-style search like 'best buy' and 'loved the color'. Dgraph took that, and the other information in the schema, and built queries and search into the API. + +Let's find all the products that were easy to install. + +```graphql +query { + queryReview(filter: { comment: {alloftext: "easy to install"}}) { + comment + by { + username + } + about { + name + } + } +} +``` + +What reviews did you get back? It'll depend on the data you added, but you'll at least get the initial review we added. + +Maybe you want to find reviews that describe best GraphQL products and give a high rating. + +```graphql +query { + queryReview(filter: { comment: {alloftext: "best GraphQL"}, rating: { ge: 10 }}) { + comment + by { + username + } + about { + name + } + } +} +``` + +How about we find the customers with names starting with "Mich" and the five products that each of those liked the most. + +```graphql +query { + queryCustomer(filter: { username: { regexp: "/Mich.*/" } }) { + username + reviews(order: { asc: rating }, first: 5) { + comment + rating + about { + name + } + } + } +} +``` + +We started with nothing more than the definition of three GraphQL types, yet already we have a running GraphQL API that keeps usernames unique, can run queries and mutations, and we are on our way for an e-commerce app. + +There's much more that could be done: we can build in more types, more powerful search, build in queries that work through the graph like a recommendation system, and more. Keep learning about GraphQL with Dgraph to find out about great things you can do. + +## Where Next + +Depending on if you need a bit more of a walkthrough or if you're off and running, you should checkout the worked example or the sample React app. + +The worked example builds through similar material to this quick start, but also works through what's allowed in your input schema and what happens to what you put in there. + +The React app is a UI for a simple social media example that's built on top of a Dgraph GraphQL instance. + +Later, as you're building your app, you'll need the reference materials on schema and server administration. \ No newline at end of file diff --git a/wiki/content/graphql/schema/_index.md b/wiki/content/graphql/schema/_index.md new file mode 100644 index 00000000000..bd8d1e29ca7 --- /dev/null +++ b/wiki/content/graphql/schema/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Schema" +[menu.main] + url = "/graphql/schema/" + identifier = "schema" + parent = "graphql" + weight = 4 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/schema/deprecated.md b/wiki/content/graphql/schema/deprecated.md new file mode 100644 index 00000000000..677f089e1e8 --- /dev/null +++ b/wiki/content/graphql/schema/deprecated.md @@ -0,0 +1,10 @@ ++++ +title = "Deprecation" +[menu.main] + parent = "schema" + weight = 7 ++++ + +Documentation about `@deprecated` directive coming soon. + + \ No newline at end of file diff --git a/wiki/content/graphql/schema/dgraph-schema.md b/wiki/content/graphql/schema/dgraph-schema.md new file mode 100644 index 00000000000..a20e5847ad0 --- /dev/null +++ b/wiki/content/graphql/schema/dgraph-schema.md @@ -0,0 +1,76 @@ ++++ +title = "Dgraph Schema Fragment" +[menu.main] + parent = "schema" + weight = 8 ++++ + +While editing your schema, you might find it useful to include this GraphQL schema fragment. It sets up the definitions of the directives, etc. (like `@search`) that you'll use in your schema. If your editor is GraphQL aware, it may give you errors if you don't have this available and context sensitive help if you do. + +Don't include it in your input schema to Dgraph - use your editing environment to set it up as an import. The details will depend on your setup. + +```graphql +scalar DateTime + +enum DgraphIndex { + int + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour +} + +input AuthRule { + and: [AuthRule] + or: [AuthRule] + not: AuthRule + rule: String +} + +enum HTTPMethod { + GET + POST + PUT + PATCH + DELETE +} + +enum Mode { + BATCH + SINGLE +} + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +directive @hasInverse(field: String!) on FIELD_DEFINITION +directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION +directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION +directive @id on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete: AuthRule) on OBJECT +directive @custom(http: CustomHTTP) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE +``` diff --git a/wiki/content/graphql/schema/documentation.md b/wiki/content/graphql/schema/documentation.md new file mode 100644 index 00000000000..837a47cbbde --- /dev/null +++ b/wiki/content/graphql/schema/documentation.md @@ -0,0 +1,14 @@ ++++ +title = "Documentation and Comments" +[menu.main] + parent = "schema" + weight = 6 ++++ + +More documentation about documentation comments coming soon :-) + +Dgraph accepts GraphQL documentation comments `"""..."""` that gets passed through to the generated API and thus shown as documentation in GraphQL tools like GraphiQL, GraphQL Playground, Insomnia etc. + +You can also add `# ...` comments where ever you like. Those are just like code comments in the input schema and get dropped. + +Any comment starting with `# Dgraph.` is reserved and shouldn't be used to document your input schema. diff --git a/wiki/content/graphql/schema/graph-links.md b/wiki/content/graphql/schema/graph-links.md new file mode 100644 index 00000000000..676fcddb261 --- /dev/null +++ b/wiki/content/graphql/schema/graph-links.md @@ -0,0 +1,126 @@ ++++ +title = "Links in the Graph" +[menu.main] + parent = "schema" + weight = 4 ++++ + +All the data in your app forms a GraphQL data graph. That graph has nodes of particular types (the types you define in your schema) and links between the nodes to form the data graph. + +Dgraph uses the types and fields in the schema to work out how to link that graph, what to accept for mutations and what shape responses should take. + +Edges in that graph are directed: either pointing in one direction or two. You use the `@hasInverse` directive to tell Dgraph how to handle two-way edges. + +### One-way Edges + +If you only ever need to traverse the graph between nodes in a particular direction, then your schema can simply contain the types and the link. + +In this schema, posts have an author - each post in the graph is linked to its author - but that edge is one-way. + +```graphql +type Author { + ... +} + +type Post { + ... + author: Author +} +``` + +You'll be able to traverse the graph from a Post to its author, but not able to traverse from an author to all their posts. Sometimes that's the right choice, but mostly, you'll want two way edges. + +Note: Dgraph won't store the reverse direction, so if you change your schema to include a `@hasInverse`, you'll need to migrate the data to add the reverse edges. + +### Two-way edges - edges with an inverse + +GraphQL schemas are always under-specified in that if we extended our schema to: + +```graphql +type Author { + ... + posts: [Post] +} + +type Post { + ... + author: Author +} +``` + +Then, the schema says that an author has a list of posts and a post has an author. But, that GraphQL schema doesn't say that every post in the list of posts for an author has the same author as their `author`. For example, it's perfectly valid for author `a1` to have a `posts` edge to post `p1`, that has an `author` edge to author `a2`. Here, we'd expect an author to be the author of all their posts, but that's not what GraphQL enforces. In GraphQL, it's left up to the implementation to make two-way connections in cases that make sense. That's just part of how GraphQL works. + +In Dgraph, the directive `@hasInverse` is used to create a two-way edge. + +```graphql +type Author { + ... + posts: [Post] @hasInverse(field: author) +} + +type Post { + ... + author: Author +} +``` + +With that, `posts` and `author` are just two directions of the same link in the graph. For example, adding a new post with + +```graphql +mutation { + addPost(input: [ + { ..., author: { username: "diggy" }} + ]) { + ... + } +} +``` + +will automatically add it to Diggy's list of `posts`. Deleting the post will remove it from Diggy's `posts`. Similarly, using an update mutation on an author to insert a new post will automatically add Diggy as the author the author + +```graphql +mutation { + updateAuthor(input: { + filter: { username: { eq: "diggy "}}, + set: { posts: [ {... new post ...}]} + }) { + ... + } +} +``` + +### Many edges + +It's not really possible to auto-detect what a schema designer meant for two-way edges. There's not even only one possible relationship between two types. Consider, for example, if an app recorded the posts an `Author` had recently liked (so it can suggest interesting material) and just a tally of all likes on a post. + +```graphql +type Author { + ... + posts: [Post] + recentlyLiked: [Post] +} + +type Post { + ... + author: Author + numLikes: Int +} +``` + +It's not possible to detect what is meant here as a one-way edge, or which edges are linked as a two-way connection. That's why `@hasInverse` is needed - so you can enforce the semantics your app needs. + +```graphql +type Author { + ... + posts: [Post] @hasInverse(field: author) + recentlyLiked: [Post] +} + +type Post { + ... + author: Author + numLikes: Int +} +``` + +Now, Dgraph will manage the connection between posts and authors and you can get on with concentrating on what your app needs to to - suggesting them interesting content. diff --git a/wiki/content/graphql/schema/ids.md b/wiki/content/graphql/schema/ids.md new file mode 100644 index 00000000000..1f07c65b210 --- /dev/null +++ b/wiki/content/graphql/schema/ids.md @@ -0,0 +1,52 @@ ++++ +title = "IDs" +[menu.main] + parent = "schema" + weight = 3 ++++ + +There's two types of identity built into Dgraph. Those are accessed via the `ID` scalar type and the `@id` directive. + +### The ID type + +In Dgraph, every node has a unique 64 bit identifier. You can, but don't have to, expose that in GraphQL via the `ID` type. `ID`s are auto-generated, immutable and never reused. Each type can have at most one `ID` field. + +The `ID` type works great for things that you'll want to refer to via an id, but don't need to set the identifier externally. Examples are things like posts, comments, tweets, etc. + +For example, you might set the following type in a schema. + +```graphql +type Post { + id: ID! + ... +} +``` + +In a single page app, you'll want to render the page for `http://.../posts/0x123` when a user clicks to view the post with id `0x123`. You app can then use a `getPost(id: "0x123") { ... }` GraphQL query to fetch the data to generate the page. + +You'll also be able to update and delete posts by id. + +### The @id directive + +For some types, you'll need a unique identifier set from outside Dgraph. A common example is a username. + +The `@id` directive tells Dgraph to keep values of that field unique and to use them as identifiers. + +For example, you might set the following type in a schema. + +```graphql +type User { + username: String! @id + ... +} +``` + +Dgraph will then require a unique username when creating a new user --- it'll generate the input type for `addUser` with `username: String!` so you can't make an add mutation without setting a username, and when processing the mutation, Dgraph will ensure that the username isn't already set for another node of the `User` type. + +Identities created with `@id` are reusable - if you delete an existing user, you can reuse the username. + +As with `ID` types, Dgraph will generate queries and mutations so you'll also be able to query, update and delete by id. + +### More to come + +We are currently considering expanding uniqueness to include composite ids and multiple unique fields (e.g. [this](https://discuss.dgraph.io/t/support-multiple-unique-fields-in-dgraph-graphql/8512) issue). diff --git a/wiki/content/graphql/schema/reserved.md b/wiki/content/graphql/schema/reserved.md new file mode 100644 index 00000000000..84228f5819f --- /dev/null +++ b/wiki/content/graphql/schema/reserved.md @@ -0,0 +1,10 @@ ++++ +title = "Reserved Names" +[menu.main] + parent = "schema" + weight = 1 ++++ + +Names `Int`, `Float`, `Boolean`, `String`, `DateTime` and `ID` are reserved and cannot be used to define any other identifiers. + +For each type, Dgraph generates a number of GraphQL types needed to operate the GraphQL API, these generated type names also can't be present in the input schema. For example, for a type `Author`, Dgraph generates `AuthorFilter`, `AuthorOrderable`, `AuthorOrder`, `AuthorRef`, `AddAuthorInput`, `UpdateAuthorInput`, `AuthorPatch`, `AddAuthorPayload`, `DeleteAuthorPayload` and `UpdateAuthorPayload`. Thus if `Author` is present in the input schema, all of those become reserved type names. \ No newline at end of file diff --git a/wiki/content/graphql/schema/schema-overview.md b/wiki/content/graphql/schema/schema-overview.md new file mode 100644 index 00000000000..36b694eec2b --- /dev/null +++ b/wiki/content/graphql/schema/schema-overview.md @@ -0,0 +1,15 @@ ++++ +title = "Overview" +[menu.main] + parent = "schema" + identifier = "schema-overview" + weight = 1 ++++ + +This section describes all the things you can put in your input GraphQL schema, and what gets generated from that. + +The process for serving GraphQL with Dgraph is to add a set of GraphQL type definitions using the `/admin` endpoint. Dgraph takes those definitions, generates queries and mutations, and serves the generated GraphQL schema. + +The input schema may contain interfaces, types and enums that follow the usual GraphQL syntax and validation rules. + +If you want to make your schema editing experience nicer, you should use an editor that does syntax highlighting for GraphQL. With that, you may also want to include the definitions [here](/graphql/schema/dgraph-schema) as an import. diff --git a/wiki/content/graphql/schema/search.md b/wiki/content/graphql/schema/search.md new file mode 100644 index 00000000000..c4a668f4bf3 --- /dev/null +++ b/wiki/content/graphql/schema/search.md @@ -0,0 +1,303 @@ ++++ +title = "Search and Filtering" +[menu.main] + parent = "schema" + identifier = "schema-search" + weight = 5 ++++ + +The `@search` directive tells Dgraph what search to build into your GraphQL API. + +When a type contains an `@search` directive, Dgraph constructs a search input type and a query in the GraphQL `Query` type. For example, if the schema contains + +```graphql +type Post { + ... +} +``` + +then Dgraph constructs a `queryPost` GraphQL query for querying posts. The `@search` directives in the `Post` type control how Dgraph builds indexes and what kinds of search it builds into `queryPost`. If the type contains + +```graphql +type Post { + ... + datePublished: DateTime @search +} +``` + +then it's possible to filter posts with a date-time search like: + +```graphql +query { + queryPost(filter: { datePublished: { ge: "2020-06-15" }}) { + ... + } +} +``` + +If the type tells Dgraph to build search capability based on a term (word) index for the `title` field + +```graphql +type Post { + ... + title: String @search(by: [term]) +} +``` + +then, the generated GraphQL API will allow search by terms in the title. + +```graphql +query { + queryPost(filter: { title: { anyofterms: "GraphQL" }}) { + ... + } +} +``` + +Dgraph also builds search into the fields of each type, so searching is available at deep levels in a query. For example, if the schema contained these types + +```graphql +type Post { + ... + title: String @search(by: [term]) +} + +type Author { + name: String @search(by: [hash]) + posts: [Post] +} +``` + +then Dgraph builds GraphQL search such that a query can, for example, find an author by name (from the hash search on `name`) and return only their posts that contain the term "GraphQL". + +```graphql +queryAuthor(filter: { name: { eq: "Diggy" } } ) { + posts(filter: { title: { anyofterms: "GraphQL" }}) { + title + } +} +``` + +There's different search possible for each type as explained below. + +### Int, Float and DateTime + +| argument | constructed filter | +|----------|----------------------| +| none | `lt`, `le`, `eq`, `ge` and `gt` | + +Search for fields of types `Int`, `Float` and `DateTime` is enabled by adding `@search` to the field with no arguments. For example, if a schema contains: + +```graphql +type Post { + ... + numLikes: Int @search +} +``` + +Dgraph generates search into the API for `numLikes` in two ways: a query for posts and field search on any post list. + +A field `queryPost` is added to the `Query` type of the schema. + +```graphql +type Query { + ... + queryPost(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] +} +``` + +`PostFilter` will contain less than `lt`, less than or equal to `le`, equal `eq`, greater than or equal to `ge` and greater than `gt` search on `numLikes`. Allowing for example: + +```graphql +query { + queryPost(filter: { numLikes: { gt: 50 }}) { + ... + } +} +``` + +Also, any field with a type of list of posts has search options added to it. For example, if the input schema also contained: + +```graphql +type Author { + ... + posts: [Post] +} +``` + +Dgraph would insert search into `posts`, with + +```graphql +type Author { + ... + posts(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] +} +``` + +That allows search within the GraphQL query. For example, to find Diggy's posts with more than 50 likes. + +```graphql +queryAuthor(filter: { name: { eq: "Diggy" } } ) { + ... + posts(filter: { numLikes: { gt: 50 }}) { + title + text + } +} +``` + +### DateTime + +| argument | constructed filters | +|----------|----------------------| +| `year`, `month`, `day`, or `hour` | `lt`, `le`, `eq`, `ge` and `gt` | + +As well as `@search` with no arguments, `DateTime` also allows specifying how the search index should be built: by year, month, day or hour. `@search` defaults to year, but once you understand your data and query patterns, you might want to changes that like `@search(by: [day])`. + +### Boolean + +| argument | constructed filter | +|----------|----------------------| +| none | `true` and `false` | + +Booleans can only be tested for true or false. If `isPublished: Boolean @search` is in the schema, then the search allows + +```graphql +filter: { isPublished: true } +``` + +and + +```graphql +filter: { isPublished: false } +``` + +### String + +Strings allow a wider variety of search options than other types. For strings, you have the following options as arguments to `@search`. + +| argument | constructed searches | +|----------|----------------------| +| `hash` | `eq` | +| `exact` | `lt`, `le`, `eq`, `ge` and `gt` (lexicographically) | +| `regexp` | `regexp` (regular expressions) | +| `term` | `allofterms` and `anyofterms` | +| `fulltext` | `alloftext` and `anyoftext` | + +* *Schema rule*: `hash` and `exact` can't be used together. + +#### String exact and hash search + +Exact and hash search has the standard lexicographic meaning. + +```graphql +query { + queryAuthor(filter: { name: { eq: "Diggy" } }) { ... } +} +``` + +And for exact search + +```graphql +query { + queryAuthor(filter: { name: { gt: "Diggy" } }) { ... } +} +``` + +to find users with names lexicographically after "Diggy". + +#### String regular expression search + +Search by regular expression requires bracketing the expression with `/` and `/`. For example, query for "Diggy" and anyone else with "iggy" in their name: + +```graphql +query { + queryAuthor(filter: { name: { regexp: "/.*iggy.*/" } }) { ... } +} +``` + +#### String term and fulltext search + +If the schema has + +```graphql +type Post { + title: String @search(by: [term]) + text: String @search(by: [fulltext]) + ... +} +``` + +then + +```graphql +query { + queryPost(filter: { title: { `allofterms: "GraphQL tutorial"` } } ) { ... } +} +``` + +will match all posts with both "GraphQL and "tutorial" in the title, while `anyofterms: "GraphQL tutorial"` would match posts with either "GraphQL" or "tutorial". + +`fulltext` search is Google-stye text search with stop words, stemming. etc. So `alloftext: "run woman"` would match "run" as well as "running", etc. For example, to find posts that talk about fantastic GraphQL tutorials: + +```graphql +query { + queryPost(filter: { title: { `alloftext: "fantastic GraphQL tutorials"` } } ) { ... } +} +``` + +#### Strings with multiple searches + +It's possible to add multiple string indexes to a field. For example to search for authors by `eq` and regular expressions, add both options to the type definition, as follows. + +```graphql +type Author { + ... + name: String! @search(by: [hash, regexp]) +} +``` + +### Enums + +| argument | constructed searches | +|----------|----------------------| +| none | `eq` | +| `hash` | `eq` | +| `exact` | `lt`, `le`, `eq`, `ge` and `gt` (lexicographically) | +| `regexp` | `regexp` (regular expressions) | + +Enums are serialized in Dgraph as strings. `@search` with no arguments is the same as `@search(by: [hash])` and provides only `eq` search. Also available for enums are `exact` and `regexp`. For hash and exact search on enums, the literal enum value, without quotes `"..."`, is used, for regexp, strings are required. For example: + +```graphql +enum Tag { + GraphQL + Database + Question + ... +} + +type Post { + ... + tags: [Tag!]! @search +} +``` + +would allow + +```graphql +query { + queryPost(filter: { tags: { eq: GraphQL } } ) { ... } +} +``` + +Which would find any post with the `GraphQL` tag. + +While `@search(by: [exact, regexp]` would also admit `lt` etc. and + +```graphql +query { + queryPost(filter: { tags: { regexp: "/.*aph.*/" } } ) { ... } +} +``` + +which is helpful for example if the enums are something like product codes where regular expressions can match a number of values. diff --git a/wiki/content/graphql/schema/types.md b/wiki/content/graphql/schema/types.md new file mode 100644 index 00000000000..c474e54f7d2 --- /dev/null +++ b/wiki/content/graphql/schema/types.md @@ -0,0 +1,133 @@ ++++ +title = "Types" +[menu.main] + parent = "schema" + weight = 2 ++++ + +This page describes how you use GraphQL types to set the Dgraph GraphQL schema. + +### Scalars + +Dgraph GraphQL comes with the standard GraphQL scalars: `Int`, `Float`, `String`, `Boolean` and `ID`. There's also a `DateTime` scalar - represented as a string in RFC3339 format. + +Scalars `Int`, `Float`, `String` and `DateTime` can be used in lists. Note that lists behave like an unordered set in Dgraph. For example: `["e1", "e1", "e2"]` may get stored as `["e2", "e1"]`, i.e., duplicate values will not be stored and order may not be preserved. + +All scalars may be nullable or non-nullable. + +The `ID` type is special. IDs are auto-generated, immutable, and can be treated as strings. Fields of type `ID` can be listed as nullable in a schema, but Dgraph will never return null. + +* *Schema rule*: `ID` lists aren't allowed - e.g. `tags: [String]` is valid, but `ids: [ID]` is not. +* *Schema rule*: Each type you define can have at most one field with type `ID`. That includes IDs implemented through interfaces. + +It's not possible to define further scalars - you'll receive an error if the input schema contains the definition of a new scalar. + +For example, the following GraphQL type uses all of the available scalars. + +```graphql +type User { + userID: ID! + name: String! + lastSignIn: DateTime + recentScores: [Float] + reputation: Int + active: Boolean +} +``` + +Scalar lists in Dgraph act more like sets, so `tags: [String]` would always contain unique tags. Similarly, `recentScores: [Float]` could never contain duplicate scores. + +### Enums + +You can define enums in your input schema. For example: + +```graphql +enum Tag { + GraphQL + Database + Question + ... +} + +type Post { + ... + tags: [Tag!]! +} +``` + +### Types + +From the built-in scalars and the enums you add, you can generate types in the usual way for GraphQL. For example: + +```graphql +enum Tag { + GraphQL + Database + Dgraph +} + +type Post { + id: ID! + title: String! + text: String + datePublished: DateTime + tags: [Tag!]! + author: Author! +} + +type Author { + id: ID! + name: String! + posts: [Post!] + friends: [Author] +} +``` + +* *Schema rule*: Lists of lists aren't accepted. For example: `multiTags: [[Tag!]]` isn't valid. +* *Schema rule*: Fields with arguments are not accepted in the input schema unless the field is implemented using the `@custom` directive. + +### Interfaces + +GraphQL interfaces allow you to define a generic pattern that multiple types follow. When a type implements an interface, that means it has all fields of the interface and some extras. + +When a type implements an interface, GraphQL requires that the type repeats all the fields from the interface, but that's just boilerplate and a maintenance problem, so Dgraph doesn't need that repetition in the input schema and will generate the correct GraphQL for you. + +For example, the following defines the schema for posts with comment threads; Dgraph will fill in the `Question` and `Comment` types to make the full GraphQL types. + +```graphql +interface Post { + id: ID! + text: String + datePublished: DateTime +} + +type Question implements Post { + title: String! +} + +type Comment implements Post { + commentsOn: Post! +} +``` + +The generated GraphQL will contain the full types, for example, `Question` gets expanded as: + +```graphql +type Question implements Post { + id: ID! + text: String + datePublished: DateTime + title: String! +} +``` + +while `Comment` gets expanded as: + +```graphql +type Comment implements Post { + id: ID! + text: String + datePublished: DateTime + commentsOn: Post! +} +``` diff --git a/wiki/content/graphql/subscriptions/index.md b/wiki/content/graphql/subscriptions/index.md new file mode 100644 index 00000000000..9553de76c24 --- /dev/null +++ b/wiki/content/graphql/subscriptions/index.md @@ -0,0 +1,38 @@ ++++ +title = "GraphQL Subscriptions" +[menu.main] + url = "/graphql/subscriptions/" + name = "Subscriptions" + identifier = "subscriptions" + parent = "graphql" + weight = 8 ++++ + +Subscriptions allow clients to listen to real-time messages from the server. The client connects to the server via a bi-directional communication channel using WebSocket and sends a subscription query that specifies which event it is interested in. When an event is triggered, the server executes the stored GraphQL query, and the result is sent through the same communication channel back to the client. + +The client can unsubscribe by sending a message to the server. The server can also unsubscribe at any time due to errors or timeout. Another significant difference between queries/mutations and a subscription is that subscriptions are stateful and require maintaining the GraphQL document, variables, and context over the lifetime of the subscription. + +![Subscription](/images/graphql/subscription_flow.png "Subscription in GraphQL") + +## Enable Subscriptions + +In GraphQL, it's straightforward to enable subscriptions on any type. We add the directive `@withSubscription` in the schema along with the type definition. + +```graphql +type Todo @withSubscription { + id: ID! + title: String! + description: String! + completed: Boolean! +} +``` + +## Example + +Once the schema is added, you can fire a subscription query, and we receive updates when the subscription query result is updated. + +![Subscription](/images/graphql/subscription_example.gif "Subscription Example") + +## Apollo Client Setup + +Here is an excellent blog explaining in detail on [how to set up GraphQL Subscriptions using Apollo client](https://dgraph.io/blog/post/how-does-graphql-subscription/). diff --git a/wiki/content/graphql/todo-app-tutorial/_index.md b/wiki/content/graphql/todo-app-tutorial/_index.md new file mode 100644 index 00000000000..8de6f4ae05f --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Todo Tutorial" +[menu.main] + url = "/graphql/todo-app-tutorial/" + identifier = "todo-app-tutorial" + parent = "graphql" + weight = 10 ++++ \ No newline at end of file diff --git a/wiki/content/graphql/todo-app-tutorial/deploy.md b/wiki/content/graphql/todo-app-tutorial/deploy.md new file mode 100644 index 00000000000..d60fc41d5d3 --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/deploy.md @@ -0,0 +1,72 @@ ++++ +title = "Deploying on Slash GraphQL" +[menu.main] + parent = "todo-app-tutorial" + weight = 6 ++++ + +*You can join the waitlist for Slash GraphQL [here](https://dgraph.io/slash-graphql).* + +Let's now deploy our fully functional app on Slash GraphQL [slash.dgraph.io](https://slash.dgraph.io). + +### Create a deployment + +After successfully logging into the site for the first time, your dashboard should look something like this. + +![Slash-GraphQL: Get Started](/images/graphql/tutorial/todo/slash-graphql-1.png) + +Let's go ahead and create a new deployment. + +![Slash-GraphQL: Create deployment](/images/graphql/tutorial/todo/slash-graphql-2.png) + +We named our deployment `todo-app-deployment` and set the optional subdomain as +`todo-app`, using which the deployment will be accessible. We can choose any +subdomain here as long as it is available. + +Let's set it up in AWS, in the US region, and click on the *Create Deployment* button. + +![Slash-GraphQL: Deployment created ](/images/graphql/tutorial/todo/slash-graphql-3.png) + +While the deployment is spinning up, remember to copy the API key, as the same API key +won't be visible again. Though, you don't need to worry too much about it since you can +create and revoke API keys from the setting page. + +Let's also copy the endpoint, which is our GraphQL API endpoint. + +Once the deployment is ready, let's add our schema there (insert your public key) by going to the schema tab. + +```graphql +type Task @auth( + query: { rule: """ + query($USER: String!) { + queryTask { + user(filter: { username: { eq: $USER } }) { + __typename + } + } + }"""}), { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} +type User { + username: String! @id @search(by: [hash]) + name: String + tasks: [Task] @hasInverse(field: user) +} +# Dgraph.Authorization X-Auth0-Token https://dgraph.io/jwt/claims RS256 "" +``` + +Once the schema is submitted successfully, we can use the GraphQL API endpoint. + +Let's update our frontend to use this URL instead of localhost. Open `src/config.json` and update the `graphqlUrl` field with your GraphQL API endpoint. + +```json +{ + ... + "graphqlUrl": "" +} +``` + +That's it! Just in two steps on Slash GraphQL (deployment & schema), we got a GraphQL API that we can now easily use in any application! diff --git a/wiki/content/graphql/todo-app-tutorial/todo-UI.md b/wiki/content/graphql/todo-app-tutorial/todo-UI.md new file mode 100644 index 00000000000..ba6583ffb9d --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-UI.md @@ -0,0 +1,330 @@ ++++ +title = "Creating basic UI" +[menu.main] + parent = "todo-app-tutorial" + weight = 3 ++++ + +In this step, we will create a simple todo app (React) and integrate it with Auth0. + +## Create React app + +Let's start by creating a React app using the `create-react-app` command. + +``` +npx create-react-app todo-react-app +``` + +To verify navigate to the folder, start the dev server, and visit [http://localhost:3000](http://localhost:3000). + +``` +cd todo-react-app +npm start +``` + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/bc235fda6e7557fc9204dd886c67f7eec7bdcadb). + +## Install dependencies + +Now, let's install the various dependencies that we will need in the app. + +``` +npm install todomvc-app-css classnames graphql-tag history react-router-dom +``` + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/fc7ed70fdde368179e9d7310202b1a0952d2c5c1). + +## Setup Apollo Client + +Let's start with installing the Apollo dependencies and then create a setup. + +``` +npm install @apollo/react-hooks apollo-cache-inmemory apollo-client apollo-link-http graphql apollo-link-context +``` + +Now, let's update our `src/App.js` with the below content to include the Apollo client setup. + +```javascript +import React from "react" + +import ApolloClient from "apollo-client" +import { InMemoryCache } from "apollo-cache-inmemory" +import { ApolloProvider } from "@apollo/react-hooks" +import { createHttpLink } from "apollo-link-http" + +import "./App.css" + +const createApolloClient = () => { + const httpLink = createHttpLink({ + uri: "http://localhost:8080/graphql", + options: { + reconnect: true, + }, + }) + + return new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + }) +} + +const App = () => { + const client = createApolloClient() + return ( + +
+

todos

+ +
+
+ ) +} + +export default App +``` + +Here we have created a simple instance of the Apollo client and passed the URL of our GraphQL API. Then we have passed the client to `ApolloProvider` and wrapped our `App` so that its accessible throughout the app. + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/f3fedc663e75d2f8ce933432b15db5d5d080ccc2). + +## Queries and Mutations + +Now, let's add some queries and mutations. + +First, let's see how we can add a todo and get todos. Create a file `src/GraphQLData.js` and add the following. + +```javascript +import gql from "graphql-tag"; + +export const ADD_TODO = gql` + mutation addTask($task: [AddTaskInput!]!) { + addTask(input: $task) { + task { + id + title + } + } + } +` +export const GET_TODOS = gql` + query { + queryTask { + id + title + completed + } + } +` +``` + +Refer to the complete set of queries and mutations [here](https://github.com/dgraph-io/graphql-sample-apps/blob/948e9a8626b1f0c1e40de02485a1110b45f53b89/todo-app-react/src/GraphQLData.js). + +Now, let's see how to use that to add a todo. +Let's import the dependencies first in `src/TodoApp.js` + +```javascript +import { useQuery, useMutation } from "@apollo/react-hooks" +import { GET_TODOS, ADD_TODO } from "./GraphQLData" +``` + +Let's now create the functions to add a todo and get todos. + +```javascript +const TodoApp = () => { + +... +const [addTodo] = useMutation(ADD_TODO); + +const { loading, error, data } = useQuery(GET_TODOS); + const getData = () => { + if (loading) { + return null; + } + if (error) { + console.error(`GET_TODOS error: ${error}`); + return `Error: ${error.message}`; + } + if (data.queryTask) { + setShownTodos(data.queryTask) + } + } + + ... + +const add = (title) => + addTodo({ + variables: { task: [ + { title: title, completed: false, user: { username: "email@example.com" } } + ]}, + refetchQueries: [{ + query: GET_TODOS + }] + }); + ... + +``` + +Refer the complete set of functions [here](https://github.com/dgraph-io/graphql-sample-apps/blob/948e9a8626b1f0c1e40de02485a1110b45f53b89/todo-app-react/src/TodoApp.js). + +Also, check the other files updated in this step and make those changes as well. + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/948e9a8626b1f0c1e40de02485a1110b45f53b89). + +## Auth0 Integration + +Now, let's integrate Auth0 in our application and use that to add the logged-in user. Let's first create an app in Auth0. + +- Head over to Auth0 and create an account. Click 'sign up' [here](https://auth0.com/) +- Once the signup is done, click "Create Application" in "Integrate Auth0 into your application". +- Give your app a name and select "Single Page Web App" application type +- Select React as the technology +- No need to do the sample app, scroll down to "Configure Auth0" and select "Application Settings". +- Select your app and add the values of `domain` and `clientid` in the file `src/auth_template.json`. Check this [link](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) for more information. +- Add `http://localhost:3000` to "Allowed Callback URLs", "Allowed Web Origins" and "Allowed Logout URLs". + +Check the commit [here](https://github.com/dgraph-io/graphql-sample-apps/commit/4c9c42e1ae64545cb10a24922623a196288d061c) for verifying the Auth0 setup you did after following the above steps. + +Let's also add definitions for getting a user and adding it to `src/GraphQLData.js`. + +```javascript +import gql from "graphql-tag"; + +export const GET_USER = gql` + query getUser($username: String!) { + getUser(username: $username) { + username + name + tasks { + id + title + completed + } + } + } +` + +export const ADD_USER = gql` + mutation addUser($user: AddUserInput!) { + addUser(input: [$user]) { + user { + username + } + } + } +` +``` + +Check the updated file [here](https://github.com/dgraph-io/graphql-sample-apps/blob/4c9c42e1ae64545cb10a24922623a196288d061c/todo-app-react/src/GraphQLData.js) + +Now, let's also add functions for these in `src/TodoApp.js`. + +```javascript +... +import { GET_USER, GET_TODOS, ADD_USER, ADD_TODO, DELETE_TODO, TOGGLE_TODO, UPDATE_TODO, CLEAR_COMPLETED_TODO, TOGGLE_ALL_TODO } from "./GraphQLData"; +import { useAuth0 } from "./react-auth0-spa"; + +... + +const useImperativeQuery = (query) => { + const { refetch } = useQuery(query, { skip: true }); + const imperativelyCallQuery = (variables) => { + return refetch(variables); + }; + return imperativelyCallQuery; +}; + +const TodoApp = () => { + + ... + const [newTodo, setNewTodo] = useState(""); + const [shownTodos, setShownTodos] = useState([]); + + const [addUser] = useMutation(ADD_USER); + + ... + + const [updateTodo] = useMutation(UPDATE_TODO); + const [clearCompletedTodo] = useMutation(CLEAR_COMPLETED_TODO); + const getUsers = useImperativeQuery(GET_USER) + + const { user } = useAuth0(); + + const createUser = () => { + if (user === undefined) { + return null; + } + const { data: getUser } = getUsers({ + username: user.email + }); + if (getUser && getUser.getUser === null) { + const newUser = { + username: user.email, + name: user.nickname, + }; + addUser({ + variables: { + user: newUser + } + }) + } + } +} + +... + +``` + +Check all the changes for the file [here](https://github.com/dgraph-io/graphql-sample-apps/blob/4c9c42e1ae64545cb10a24922623a196288d061c/todo-app-react/src/TodoApp.js) + +Let's create a short profile page to display user details. Add files `src/Profile.js` and `src/Profile.css`. + +```javascript +import React from "react"; +import { useAuth0 } from "./react-auth0-spa"; +import './Profile.css'; + +const Profile = () => { + const { loading, user } = useAuth0(); + + if (loading || !user) { + return
Loading...
; + } + + return ( +
+ Profile +

Name: {user.nickname}

+

Email: {user.email}

+
+ ); +}; + +export default Profile; +``` + +```css +.profile { + padding: 15px; +} +.profile-img { + display: block; + margin: 0 auto; + border-radius: 50%; +} +``` + +Also, check the other files updated in this step and make those changes as well. + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/4c9c42e1ae64545cb10a24922623a196288d061c). + +Let's now start the app. + +``` +npm start +``` + +Now you should have an app running! diff --git a/wiki/content/graphql/todo-app-tutorial/todo-auth-rules.md b/wiki/content/graphql/todo-app-tutorial/todo-auth-rules.md new file mode 100644 index 00000000000..03cecd024e1 --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-auth-rules.md @@ -0,0 +1,57 @@ ++++ +title = "Auth Rules" +[menu.main] + parent = "todo-app-tutorial" + weight = 4 ++++ + +In the current state of the app, we can view anyone's todos, but we want our todos to be private to us. Let's do that using the `auth` directive to limit that to the user's todos. + +We want to limit the user to its own todos, so we will define the query in `auth` to filter depending on the user's username. + +Let's update the schema to include that, and then let's understand what is happening there - + +```graphql +type Task @auth( + query: { rule: """ + query($USER: String!) { + queryTask { + user(filter: { username: { eq: $USER } }) { + __typename + } + } + }"""}){ + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} +type User { + username: String! @id @search(by: [hash]) + name: String + tasks: [Task] @hasInverse(field: user) +} +``` + +Resubmit the updated schema - +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +Now let's see what does the definition inside the `auth` directive means. Firstly, we can see that this rule applies to `query` (similarly we can define rules on `add`, `update` etc.). + +```graphql + query ($USER: String!) { + queryTask { + user(filter: {username: {eq: $USER}}) { + __typename + } + } +} +``` + +The rule contains a parameter `USER` which we will use to filter the todos by a user. As we know `queryTask` returns an array of `task` that contains the `user` also and we want to filter it by `user`, so we compare the `username` of the user with the `USER` passed to the auth rule (logged in user). + +Now the next thing you would be wondering is that how do we pass a value for the `USER` parameter in the auth rule since its not something that you can call, the answer is pretty simple actually that value will be extracted from the JWT token which we pass to our GraphQL API as a header and then it will execute the rule. + +Let's see how we can do that in the next step using Auth0 as an example. diff --git a/wiki/content/graphql/todo-app-tutorial/todo-auth0-jwt.md b/wiki/content/graphql/todo-app-tutorial/todo-auth0-jwt.md new file mode 100644 index 00000000000..2ed2f11e5a6 --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-auth0-jwt.md @@ -0,0 +1,214 @@ ++++ +title = "Using Auth0" +[menu.main] + parent = "todo-app-tutorial" + weight = 5 ++++ + +Let's start by going to our Auth0 dashboard where we can see the application which we have already created and used in our frontend-application. + +![Dashboard](/images/graphql/tutorial/todo/dashboard.png) + +Now we want to use the JWT that Auth0 generates, but we also need to add custom claims to that token which will be used by our auth rules. +So we can use something known as "Rules" (left sidebar on dashboard page) to add custom claims to a token. Let's create a new empty rule. + +![Rule](/images/graphql/tutorial/todo/rule.png) + +Replace the content with the the following - +```javascript +function (user, context, callback) { + const namespace = "https://dgraph.io/jwt/claims"; + context.idToken[namespace] = + { + 'USER': user.email, + }; + + return callback(null, user, context); +} +``` + +In the above function, we are only just adding the custom claim to the token with a field as `USER` which if you recall from the last step is used in our auth rules, so it needs to match exactly with that name. + +Now let's go to `Settings` of our Auth0 application and then go down to view the `Advanced Settings` to check the JWT signature algorithm (OAuth tab) and then get the certificate (Certificates tab). We will be using `RS256` in this example so let's make sure it's set to that and then copy the certificate which we will use to get the public key. Use the download certificate button there to get the certificate in `PEM`. + +![Certificate](/images/graphql/tutorial/todo/certificate.png) + +Now let's run a command to get the public key from it, which we will add to our schema. Just change the `file_name` and run the command. + +``` +openssl x509 -pubkey -noout -in file_name.pem +``` + +Copy the public key and now let's add it to our schema. For doing that we will add something like this, to the bottom of our schema file - + +``` +# Dgraph.Authorization X-Auth0-Token https://dgraph.io/jwt/claims RS256 "" +``` + +Let me just quickly explain what each thing means in that, so firstly we start the line with a `# Dgraph.Authorization`, next is the name of the header `X-Auth0-Token` (can be anything) which will be used to send the value of the JWT. Next is the custom-claim name `https://dgraph.io/jwt/claims` (again can be anything, just needs to match with the name specified in Auth0). Then next is the `RS256` the JWT signature algorithm (another option is `HS256` but remember to use the same algorithm in Auth0) and lastly, update `` with your public key within the quotes and make sure to have it in a single line and add `\n` where ever needed. The updated schema will look something like this (update the public key with your key) - + +```graphql +type Task @auth( + query: { rule: """ + query($USER: String!) { + queryTask { + user(filter: { username: { eq: $USER } }) { + __typename + } + } + }"""}), { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} +type User { + username: String! @id @search(by: [hash]) + name: String + tasks: [Task] @hasInverse(field: user) +} +# Dgraph.Authorization X-Auth0-Token https://dgraph.io/jwt/claims RS256 "" +``` + +Resubmit the updated schema - +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +Let's get that token and see what all it contains, then update the frontend accordingly. For doing this, let's start our app again. + +``` +npm start +``` + +Now open a browser window, navigate to [http://localhost:3000](http://localhost:3000) and open the developer tools, go to the `network` tab and find a call called `token` to get your JWT from its response JSON (field `id_token`). + +![Token](/images/graphql/tutorial/todo/token.png) + +Now go to [jwt.io](https://jwt.io) and paste your token there. + +![jwt](/images/graphql/tutorial/todo/jwt.png) + +The token also includes our custom claim like below. + +```json +{ +"https://dgraph.io/jwt/claims": { + "USER": "vardhanapoorv" + }, + ... +} + ``` + +Now, you can check if the auth rule that we added is working as expected or not. Open the GraphQL tool (Insomnia, GraphQL Playground) add the URL along with the header `X-Auth0-Token` and its value as the JWT. Let's try the query to see the todos and only the todos the logged-in user created should be visible. +```graphql +query { + queryTask { + title + completed + user { + username + } + } +} +``` + +The above should give you only your todos and verifies that our auth rule worked! + +Now let's update our frontend application to include the `X-Auth0-Token` header with value as JWT from Auth0 when sending a request. + +To do this, we need to update the Apollo client setup to include the header while sending the request, and we need to get the JWT from Auth0. + +The value we want is in the field `idToken` from Auth0. We get that by quickly updating `react-auth0-spa.js` to get `idToken` and pass it as a prop to our `App`. + +```javascript +... + +const [popupOpen, setPopupOpen] = useState(false); +const [idToken, setIdToken] = useState(""); + +... + +if (isAuthenticated) { + const user = await auth0FromHook.getUser(); + setUser(user); + const idTokenClaims = await auth0FromHook.getIdTokenClaims(); + setIdToken(idTokenClaims.__raw); +} + +... + +const user = await auth0Client.getUser(); +const idTokenClaims = await auth0Client.getIdTokenClaims(); + +setIdToken(idTokenClaims.__raw); + +... + +{children} + + + +... + +``` + +Check the updated file [here](https://github.com/dgraph-io/graphql-sample-apps/blob/c94b6eb1cec051238b81482a049100b1cd15bbf7/todo-app-react/src/react-auth0-spa.js) + + Now let's use that token while creating an Apollo client instance and give it to a header `X-Auth0-Token` in our case. Let's update our `src/App.js` file. + +```javascript +... + +import { useAuth0 } from "./react-auth0-spa"; +import { setContext } from "apollo-link-context"; + +// Updated to take token +const createApolloClient = token => { + const httpLink = createHttpLink({ + uri: config.graphqlUrl, + options: { + reconnect: true, + }, +}); + +// Add header +const authLink = setContext((_, { headers }) => { + // return the headers to the context so httpLink can read them + return { + headers: { + ...headers, + "X-Auth-Token": token, + }, + }; +}); + +// Include header +return new ApolloClient({ + link: httpLink, + link: authLink.concat(httpLink), + cache: new InMemoryCache() +}); + +// Get token from props and pass to function +const App = ({idToken}) => { + const { loading } = useAuth0(); + if (loading) { + return
Loading...
; + } +const client = createApolloClient(idToken); + +... +``` + +Check the updated file [here](https://github.com/dgraph-io/graphql-sample-apps/blob/c94b6eb1cec051238b81482a049100b1cd15bbf7/todo-app-react/src/App.js). + +Refer this step in [GitHub](https://github.com/dgraph-io/graphql-sample-apps/commit/c94b6eb1cec051238b81482a049100b1cd15bbf7). + +Let's now start the app. + +``` +npm start +``` + +Now you should have an app running with Auth0! diff --git a/wiki/content/graphql/todo-app-tutorial/todo-overview.md b/wiki/content/graphql/todo-app-tutorial/todo-overview.md new file mode 100644 index 00000000000..74bd8ae9a0a --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-overview.md @@ -0,0 +1,19 @@ ++++ +title = "Overview" +[menu.main] + parent = "todo-app-tutorial" + identifier = "todo-overview" + weight = 1 ++++ + +This is a simple tutorial which will take you through making a basic todo app using Dgraph's GraphQL API and integrating it with Auth0. + +### Steps + +- [Schema Design](/graphql/todo-app-tutorial/todo-schema-design) +- [Basic UI](/graphql/todo-app-tutorial/todo-ui) +- [Add Auth Rules](/graphql/todo-app-tutorial/todo-auth-rules) +- [Use Auth0's JWT](/graphql/todo-app-tutorial/todo-auth0-jwt) +- [Deploy on Slash GraphQL](/graphql/todo-app-tutorial/deploy) + +--- diff --git a/wiki/content/graphql/todo-app-tutorial/todo-schema-design.md b/wiki/content/graphql/todo-app-tutorial/todo-schema-design.md new file mode 100644 index 00000000000..14e042d3c89 --- /dev/null +++ b/wiki/content/graphql/todo-app-tutorial/todo-schema-design.md @@ -0,0 +1,251 @@ ++++ +title = "Schema Design" +[menu.main] + parent = "todo-app-tutorial" + weight = 2 ++++ + +Let's start with listing down the entities that are involved in a basic todo app. +- Task +- User + +![Todo Graph](/images/graphql/tutorial/todo/todo-graph.png) + +Equivalent GraphQL schema for the graph above would be as follow: + +```graphql +type Task { + ... +} + +type User { + ... +} +``` + +What are the fields that these two simple entities contain? + +We have a title and a status to check if it was completed or not in the `Task` type. +Then the `User` type has a username (unique identifier), name and the tasks. + +So each user can have many tasks. + +![Todo Graph complete](/images/graphql/tutorial/todo/todo-graph-2.png) +*Note - ' \* ' signifies one-to-many relationship + +Now let's add `@id` directive to `username ` which makes it the unique key & also add `@hasInverse` directive to enable the above relationship between tasks and user. +We represent that in the GraphQL schema shown below: + +```graphql +type Task { + id: ID! + title: String! + completed: Boolean! + user: User! +} + +type User { + username: String! @id + name: String + tasks: [Task] @hasInverse(field: user) +} +``` + +Save the content in a file `schema.graphql`. + +## Running + +Before we begin, make sure that you have [Docker](https://docs.docker.com/install/) +installed on your machine. + +Let's begin by starting Dgraph standalone by running the command below: + +``` +docker run -it -p 8080:8080 dgraph/standalone:master +``` + +Let's load up the GraphQL schema file to Dgraph: + +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +You can access that GraphQL endpoint with any of the great GraphQL developer tools. +Good choices include GraphQL Playground, Insomnia, GraphiQL and Altair. + +Set up any of them and point it at `http://localhost:8080/graphql`. If you know lots about GraphQL, you might want to explore the schema, queries and mutations that were generated from the schema. + +## Mutating Data + +Let's add a user and some todos in our Todo App. + +```graphql +mutation { + addUser(input: [ + { + username: "alice@dgraph.io", + name: "Alice", + tasks: [ + { + title: "Avoid touching your face", + completed: false, + }, + { + title: "Stay safe", + completed: false + }, + { + title: "Avoid crowd", + completed: true, + }, + { + title: "Wash your hands often", + completed: true + } + ] + } + ]) { + user { + username + name + tasks { + id + title + } + } + } +} +``` + +## Querying Data + +Let's fetch the todos to list in our Todo App: + +```graphql +query { + queryTask { + id + title + completed + user { + username + } + } +} +``` + +Running the query above should return JSON response as shown below: + +```json +{ + "data": { + "queryTask": [ + { + "id": "0x3", + "title": "Avoid touching your face", + "completed": false, + "user": { + "username": "alice@dgraph.io" + } + }, + { + "id": "0x4", + "title": "Stay safe", + "completed": false, + "user": { + "username": "alice@dgraph.io" + } + }, + { + "id": "0x5", + "title": "Avoid crowd", + "completed": true, + "user": { + "username": "alice@dgraph.io" + } + }, + { + "id": "0x6", + "title": "Wash your hands often", + "completed": true, + "user": { + "username": "alice@dgraph.io" + } + } + ] + } +} +``` + +## Querying Data with Filters + +Before we get into querying data with filters, we will be required +to define search indexes to the specific fields. + +Let's say we have to run a query on the _completed_ field, for which +we add `@search` directive to the field, as shown in the schema below: + +```graphql +type Task { + id: ID! + title: String! + completed: Boolean! @search + user: User! +} +``` + +The `@search` directive is added to support the native search indexes of **Dgraph**. + +Resubmit the updated schema - +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +Now, let's fetch all todos which are completed : + +```graphql +query { + queryTask(filter: { + completed: true + }) { + title + completed + } +} +``` + +Next, let's say we have to run a query on the _title_ field, for which +we add another `@search` directive to the field, as shown in the schema below: + +```graphql +type Task { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} +``` + +The `fulltext` search index provides the advanced search capability to perform equality +comparison as well as matching with language-specific stemming and stopwords. + +Resubmit the updated schema - +``` +curl -X POST localhost:8080/admin/schema --data-binary '@schema.graphql' +``` + +Now, let's try to fetch todos whose title has the word _"avoid"_ : + +```graphql +query { + queryTask(filter: { + title: { + alloftext: "avoid" + } + }) { + id + title + completed + } +} +``` diff --git a/wiki/content/howto/loading-csv-data.md b/wiki/content/howto/loading-csv-data.md index 5984276f1ba..ad152f42ea8 100644 --- a/wiki/content/howto/loading-csv-data.md +++ b/wiki/content/howto/loading-csv-data.md @@ -73,7 +73,7 @@ _:d,_:a ``` {{% notice "note" %}} -To reuse existing integer IDs from a CSV file as UIDs in Dgraph, use Dgraph Zero's [assign endpoint]({{< relref "deploy/index.md#more-about-dgraph-zero" >}}) before data loading to allocate a range of UIDs that can be safely assigned. +To reuse existing integer IDs from a CSV file as UIDs in Dgraph, use Dgraph Zero's [assign endpoint]({{< relref "deploy/dgraph-zero" >}}) before data loading to allocate a range of UIDs that can be safely assigned. {{% /notice %}} To get the correct JSON format, you can convert the CSV into JSON and use `jq` diff --git a/wiki/content/mutations/_index.md b/wiki/content/mutations/_index.md index 23c79d346b7..6c111fb5b4e 100644 --- a/wiki/content/mutations/_index.md +++ b/wiki/content/mutations/_index.md @@ -4,6 +4,7 @@ title = "Mutations" [menu.main] url = "/mutations/" identifier = "mutations" + parent = "dql" weight = 6 +++ diff --git a/wiki/content/mutations/batch-mutations.md b/wiki/content/mutations/batch-mutations.md index 7d7951648b4..b3a9dfa41bd 100644 --- a/wiki/content/mutations/batch-mutations.md +++ b/wiki/content/mutations/batch-mutations.md @@ -13,4 +13,4 @@ Each mutation may contain multiple RDF triples. For large data uploads many such ```sh dgraph live --help ``` -See also [Fast Data Loading](/deploy#fast-data-loading). \ No newline at end of file +See also [Fast Data Loading]({{< relref "deploy/fast-data-loading.md" >}}). diff --git a/wiki/content/mutations/json-mutation-format.md b/wiki/content/mutations/json-mutation-format.md index 2bee769dd5a..c98ee18ea85 100644 --- a/wiki/content/mutations/json-mutation-format.md +++ b/wiki/content/mutations/json-mutation-format.md @@ -236,7 +236,7 @@ All edges for a predicate emanating from a single node can be deleted at once If no predicates are specified, then all of the node's known outbound edges (to other nodes and to literal values) are deleted (corresponding to deleting `S * *`). The predicates to delete are derived using the type system. Refer to the -[RDF format]({{< relref "#delete" >}}) documentation and the section on the +[RDF format]({{< relref "mutations/delete.md" >}}) documentation and the section on the [type system]({{< relref "query-language/type-system.md" >}}) for more information: @@ -609,4 +609,4 @@ cat data.json | jq '{set: .}' } ] } -``` \ No newline at end of file +``` diff --git a/wiki/content/mutations/language-rdf-types.md b/wiki/content/mutations/language-rdf-types.md index 0800fee5d07..89c8219cf00 100644 --- a/wiki/content/mutations/language-rdf-types.md +++ b/wiki/content/mutations/language-rdf-types.md @@ -46,4 +46,4 @@ The supported [RDF datatypes](https://www.w3.org/TR/rdf11-concepts/#section-Data | <http://www.w3.org/2001/XMLSchema#float> | `float` | -See the section on [RDF schema types]({{< relref "#rdf-types" >}}) to understand how RDF types affect mutations and storage. +See the section on [RDF schema types]({{< relref "query-language/schema.md#rdf-types" >}}) to understand how RDF types affect mutations and storage. diff --git a/wiki/content/mutations/upsert-block.md b/wiki/content/mutations/upsert-block.md index baefdaccfd5..8e8c587c8c8 100644 --- a/wiki/content/mutations/upsert-block.md +++ b/wiki/content/mutations/upsert-block.md @@ -195,7 +195,7 @@ curl -H "Content-Type: application/json" -X POST localhost:8080/mutate?commitNow ``` If we want to execute the mutation only when the user exists, we could use -[Conditional Upsert]({{< relref "#conditional-upsert" >}}). +[Conditional Upsert]({{< relref "mutations/conditional-upsert.md" >}}). ## Example of `val` Function @@ -292,4 +292,4 @@ curl -H "Content-Type: application/json" -X POST localhost:8080/mutate?commitNow "age": null } }' | jq -``` \ No newline at end of file +``` diff --git a/wiki/content/query-language/_index.md b/wiki/content/query-language/_index.md index f9ad229f3f1..65464bcca4f 100644 --- a/wiki/content/query-language/_index.md +++ b/wiki/content/query-language/_index.md @@ -4,6 +4,6 @@ title = "Query Language" [menu.main] url = "/query-language/" identifier = "query-language" + parent = "dql" weight = 4 +++ - diff --git a/wiki/content/query-language/aggregation.md b/wiki/content/query-language/aggregation.md index 8500e5dd1d3..0d1e63dbf92 100644 --- a/wiki/content/query-language/aggregation.md +++ b/wiki/content/query-language/aggregation.md @@ -22,7 +22,7 @@ Schema Types: | `min` / `max` | `int`, `float`, `string`, `dateTime`, `default` | | `sum` / `avg` | `int`, `float` | -Aggregation can only be applied to [value variables]({{< relref "#value-variables">}}). An index is not required (the values have already been found and stored in the value variable mapping). +Aggregation can only be applied to [value variables]({{< relref "query-language/value-variables.md">}}). An index is not required (the values have already been found and stored in the value variable mapping). An aggregation is applied at the query block enclosing the variable definition. As opposed to query variables and value variables, which are global, aggregation is computed locally. For example: ``` diff --git a/wiki/content/query-language/count.md b/wiki/content/query-language/count.md index e7e42dd8831..eea7a7b3c0a 100644 --- a/wiki/content/query-language/count.md +++ b/wiki/content/query-language/count.md @@ -26,9 +26,9 @@ Query Example: The number of films acted in by each actor with `Orlando` in thei } {{< /runnable >}} -Count can be used at root and [aliased]({{< relref "#alias">}}). +Count can be used at root and [aliased]({{< relref "query-language/alias.md" >}}). -Query Example: Count of directors who have directed more than five films. When used at the query root, the [count index]({{< relref "#count-index">}}) is required. +Query Example: Count of directors who have directed more than five films. When used at the query root, the [count index]({{< relref "query-language/schema.md#count-index" >}}) is required. {{< runnable >}} { @@ -39,7 +39,7 @@ Query Example: Count of directors who have directed more than five films. When {{< /runnable >}} -Count can be assigned to a [value variable]({{< relref "#value-variables">}}). +Count can be assigned to a [value variable]({{< relref "query-language/value-variables.md">}}). Query Example: The actors of Ang Lee's "Eat Drink Man Woman" ordered by the number of movies acted in. diff --git a/wiki/content/query-language/expand-predicates.md b/wiki/content/query-language/expand-predicates.md index bd5f94dd828..cc639241661 100644 --- a/wiki/content/query-language/expand-predicates.md +++ b/wiki/content/query-language/expand-predicates.md @@ -7,7 +7,7 @@ title = "Expand Predicates" +++ The `expand()` function can be used to expand the predicates out of a node. To -use `expand()`, the [type system]({{< relref "#type-system" >}}) is required. +use `expand()`, the [type system]({{< relref "query-language/type-system.md" >}}) is required. Refer to the section on the type system to check how to set the types nodes. The rest of this section assumes familiarity with that section. @@ -66,7 +66,7 @@ veterinarian {{% notice "note" %}} For `string` predicates, `expand` only returns values not tagged with a language -(see [language preference]({{< relref "#language-support" >}})). So it's often +(see [language preference]({{< relref "query-language/graphql-fundamentals.md#language-support" >}})). So it's often required to add `name@fr` or `name@.` as well to an expand query. {{% /notice %}} @@ -81,4 +81,4 @@ Please note that other type of filters and directives are not currently supporte with the expand function. The filter needs to use the `type` function for the filter to be allowed. Logical `AND` and `OR` operations are allowed. For example, `expand(_all_) @filter(type(Person) OR type(Animal))` will only expand -the edges that point to nodes of either type. \ No newline at end of file +the edges that point to nodes of either type. diff --git a/wiki/content/query-language/facets.md b/wiki/content/query-language/facets.md index 4a8fce4626c..210fca3e3c2 100644 --- a/wiki/content/query-language/facets.md +++ b/wiki/content/query-language/facets.md @@ -120,7 +120,7 @@ All facets on an edge are queried with `@facets`. ## Facets i18n -Facets keys and values can use language-specific characters directly when mutating. But facet keys need to be enclosed in angle brackets `<>` when querying. This is similar to predicates. See [Predicates i18n](#predicates-i18n) for more info. +Facets keys and values can use language-specific characters directly when mutating. But facet keys need to be enclosed in angle brackets `<>` when querying. This is similar to predicates. See [Predicates i18n]({{< relref "query-language/schema.md#predicates-i18n" >}}) for more info. {{% notice "note" %}}Dgraph supports [Internationalized Resource Identifiers](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier) (IRIs) for facet keys when querying.{{% /notice %}} @@ -288,7 +288,7 @@ Charlie by their `rating` which is a facet. ## Assigning Facet values to a variable -Facets on UID edges can be stored in [value variables]({{< relref "#value-variables" >}}). The variable is a map from the edge target to the facet value. +Facets on UID edges can be stored in [value variables]({{< relref "query-language/value-variables.md" >}}). The variable is a map from the edge target to the facet value. Alice's friends reported by variables for `close` and `relative`. {{< runnable >}} @@ -312,7 +312,7 @@ Alice's friends reported by variables for `close` and `relative`. ## Facets and Variable Propagation -Facet values of `int` and `float` can be assigned to variables and thus the [values propagate]({{< relref "#variable-propagation" >}}). +Facet values of `int` and `float` can be assigned to variables and thus the [values propagate]({{< relref "query-language/value-variables.md#variable-propagation" >}}). Alice, Bob and Charlie each rated every movie. A value variable on facet `rating` maps movies to ratings. A query that reaches a movie through multiple paths sums the ratings on each path. The following sums Alice, Bob and Charlie's ratings for the three movies. @@ -386,4 +386,4 @@ Calculating the average ratings of users requires a variable that maps users to val(avg_rating) } } -{{}} \ No newline at end of file +{{}} diff --git a/wiki/content/query-language/functions.md b/wiki/content/query-language/functions.md index d334ce4631c..18804b8667f 100644 --- a/wiki/content/query-language/functions.md +++ b/wiki/content/query-language/functions.md @@ -6,14 +6,14 @@ title = "Functions" weight = 2 +++ -Functions allow filtering based on properties of nodes or [variables]({{}}). Functions can be applied in the query root or in filters. +Functions allow filtering based on properties of nodes or [variables]({{}}). Functions can be applied in the query root or in filters. {{% notice "note" %}}Support for filters on non-indexed predicates was added with Dgraph `v1.2.0`. {{% /notice %}} Comparison functions (`eq`, `ge`, `gt`, `le`, `lt`) in the query root (aka `func:`) can only -be applied on [indexed predicates]({{< relref "#indexing">}}). Since v1.2, comparison functions -can now be used on [@filter]({{}}) directives even on predicates +be applied on [indexed predicates]({{< relref "query-language/schema.md#indexing" >}}). Since v1.2, comparison functions +can now be used on [@filter]({{}}) directives even on predicates that have not been indexed. Filtering on non-indexed predicates can be slow for large datasets, as they require iterating over all of the possible values at the level where the filter is being used. @@ -76,7 +76,7 @@ Index Required: `term` Matches strings that have any of the specified terms in any order; case insensitive. #### Usage at root -Query Example: All nodes that have a `name` containing either `poison` or `peacock`. Many of the returned nodes are movies, but people like Joan Peacock also meet the search terms because without a [cascade directive]({{< relref "#cascade-directive">}}) the query doesn't require a genre. +Query Example: All nodes that have a `name` containing either `poison` or `peacock`. Many of the returned nodes are movies, but people like Joan Peacock also meet the search terms because without a [cascade directive]({{< relref "query-language/cascade-directive.md">}}) the query doesn't require a genre. {{< runnable >}} { @@ -364,7 +364,7 @@ Query Example: Movies with directors with `Steven` in `name` and have directed m -Query Example: A movie in each genre that has over 30000 movies. Because there is no order specified on genres, the order will be by UID. The [count index]({{< relref "#count-index">}}) records the number of edges out of nodes and makes such queries more . +Query Example: A movie in each genre that has over 30000 movies. Because there is no order specified on genres, the order will be by UID. The [count index]({{< relref "query-language/schema.md#count-index">}}) records the number of edges out of nodes and makes such queries more . {{< runnable >}} { diff --git a/wiki/content/query-language/graphql-fundamentals.md b/wiki/content/query-language/graphql-fundamentals.md index 5dc75a86ec5..02aabbf06eb 100644 --- a/wiki/content/query-language/graphql-fundamentals.md +++ b/wiki/content/query-language/graphql-fundamentals.md @@ -194,7 +194,7 @@ above. --- -In [full-text search functions]({{< relref "#full-text-search" >}}) +In [full-text search functions]({{< relref "query-language/functions.md#full-text-search" >}}) (`alloftext`, `anyoftext`), when no language is specified (untagged or `@.`), the default (English) full-text tokenizer is used. This does not mean that the value with the `en` tag will be searched when querying the untagged value, diff --git a/wiki/content/query-language/kshortest-path-quries.md b/wiki/content/query-language/kshortest-path-quries.md index b23603ad418..94744fe887c 100644 --- a/wiki/content/query-language/kshortest-path-quries.md +++ b/wiki/content/query-language/kshortest-path-quries.md @@ -100,7 +100,7 @@ curl -H "Content-Type: application/graphql+-" localhost:8080/query -XPOST -d $'{ }' | python -m json.tool | less ``` -{{% notice "note" %}}In the query above, instead of using UID literals, we query both people using var blocks and the `uid()` function. You can also combine it with [GraphQL Variables]({{< relref "#graphql-variables" >}}).{{% /notice %}} +{{% notice "note" %}}In the query above, instead of using UID literals, we query both people using var blocks and the `uid()` function. You can also combine it with [GraphQL Variables]({{< relref "query-language/graphql-variables.md" >}}).{{% /notice %}} Edges weights are included by using facets on the edges as follows. @@ -196,4 +196,4 @@ Some points to keep in mind for shortest path queries: - Only one facet per predicate in the shortest query block is allowed. - Only one `shortest` path block is allowed per query. Only one `_path_` is returned in the result. For queries with `numpaths` > 1, `_path_` contains all the paths. - Cyclical paths are not included in the result of k-shortest path query. -- For k-shortest paths (when `numpaths` > 1), the result of the shortest path query variable will only return a single path which will be the shortest path among the k paths. All k paths are returned in `_path_`. \ No newline at end of file +- For k-shortest paths (when `numpaths` > 1), the result of the shortest path query variable will only return a single path which will be the shortest path among the k paths. All k paths are returned in `_path_`. diff --git a/wiki/content/query-language/pagination.md b/wiki/content/query-language/pagination.md index 2e54da702ee..7a691f5bf91 100644 --- a/wiki/content/query-language/pagination.md +++ b/wiki/content/query-language/pagination.md @@ -8,7 +8,7 @@ title = "Pagination" Pagination allows returning only a portion, rather than the whole, result set. This can be useful for top-k style queries as well as to reduce the size of the result set for client side processing or to allow paged access to results. -Pagination is often used with [sorting]({{< relref "#sorting">}}). +Pagination is often used with [sorting]({{< relref "query-language/sorting.md">}}). {{% notice "note" %}}Without a sort order specified, the results are sorted by `uid`, which is assigned randomly. So the ordering, while deterministic, might not be what you expected.{{% /notice %}} ## First diff --git a/wiki/content/query-language/schema.md b/wiki/content/query-language/schema.md index 66cd59f7b68..cb7265e51d2 100644 --- a/wiki/content/query-language/schema.md +++ b/wiki/content/query-language/schema.md @@ -258,7 +258,7 @@ email: string @index(exact) @noconflict . Dgraph supports a number of [RDF types in mutations]({{< relref "mutations/language-rdf-types.md" >}}). -As well as implying a schema type for a [first mutation]({{< relref "#schema" >}}), an RDF type can override a schema type for storage. +As well as implying a schema type for a [first mutation]({{< relref "query-language/schema.md" >}}), an RDF type can override a schema type for storage. If a predicate has a schema type and a mutation has an RDF type with a different underlying Dgraph type, the convertibility to schema type is checked, and an error is thrown if they are incompatible, but the value is stored in the RDF type's corresponding Dgraph type. Query results are always returned in schema type. @@ -355,7 +355,7 @@ output: ## Indexing -{{% notice "note" %}}Filtering on a predicate by applying a [function]({{< relref "#functions" >}}) requires an index.{{% /notice %}} +{{% notice "note" %}}Filtering on a predicate by applying a [function]({{< relref "query-language/functions.md" >}}) requires an index.{{% /notice %}} When filtering by applying a function, Dgraph uses the index to make the search through a potentially large dataset efficient. @@ -435,8 +435,7 @@ For predicates with the `@count` Dgraph indexes the number of edges out of each ## List Type Predicate with scalar types can also store a list of values if specified in the schema. The scalar -type needs to be enclosed within `[]` to indicate that its a list type. These lists are like an -unordered set. +type needs to be enclosed within `[]` to indicate that its a list type. ``` occupations: [string] . @@ -446,9 +445,9 @@ score: [int] . * A set operation adds to the list of values. The order of the stored values is non-deterministic. * A delete operation deletes the value from the list. * Querying for these predicates would return the list in an array. -* Indexes can be applied on predicates which have a list type and you can use [Functions]({{}}) on them. +* Indexes can be applied on predicates which have a list type and you can use [Functions]({{}}) on them. * Sorting is not allowed using these predicates. +* These lists are like an unordered set. For example: `["e1", "e1", "e2"]` may get stored as `["e2", "e1"]`, i.e., duplicate values will not be stored and order may not be preserved. ## Filtering on list diff --git a/wiki/content/query-language/sorting.md b/wiki/content/query-language/sorting.md index b73f9558e31..3aa02012652 100644 --- a/wiki/content/query-language/sorting.md +++ b/wiki/content/query-language/sorting.md @@ -18,9 +18,9 @@ Sortable Types: `int`, `float`, `String`, `dateTime`, `default` Results can be sorted in ascending order (`orderasc`) or descending order (`orderdesc`) by a predicate or variable. -For sorting on predicates with [sortable indices]({{< relref "#sortable-indices">}}), Dgraph sorts on the values and with the index in parallel and returns whichever result is computed first. +For sorting on predicates with [sortable indices]({{< relref "query-language/schema.md#sortable-indices">}}), Dgraph sorts on the values and with the index in parallel and returns whichever result is computed first. -Sorted queries retrieve up to 1000 results by default. This can be changed with [first]({{< relref "#first">}}). +Sorted queries retrieve up to 1000 results by default. This can be changed with [first]({{< relref "query-language/pagination.md#first">}}). Query Example: French director Jean-Pierre Jeunet's movies sorted by release date. @@ -73,4 +73,4 @@ that have the same first_name sort them by last_name in descending order. last_name } } -``` \ No newline at end of file +``` diff --git a/wiki/content/query-language/type-system.md b/wiki/content/query-language/type-system.md index 5f26deb8210..033010b233b 100644 --- a/wiki/content/query-language/type-system.md +++ b/wiki/content/query-language/type-system.md @@ -146,5 +146,5 @@ err := c.Alter(context.Background(), &api.Operation{ ## Expand queries and types -Queries using [expand]({{< relref "#expand-predicates" >}}) (i.e.: -`expand(_all_)`) require that the nodes to be expanded have types. \ No newline at end of file +Queries using [expand]({{< relref "query-language/expand-predicates.md" >}}) (i.e.: +`expand(_all_)`) require that the nodes to be expanded have types. diff --git a/wiki/content/query-language/value-variables.md b/wiki/content/query-language/value-variables.md index 37158511751..e92dd70ce3d 100644 --- a/wiki/content/query-language/value-variables.md +++ b/wiki/content/query-language/value-variables.md @@ -24,7 +24,7 @@ It is an error to define a value variable but not use it elsewhere in the query. Value variables are used by extracting the values with `val(var-name)`, or by extracting the UIDs with `uid(var-name)`. -[Facet]({{< relref "#facets-edge-attributes">}}) values can be stored in value variables. +[Facet]({{< relref "query-language/facets.md">}}) values can be stored in value variables. Query Example: The number of movie roles played by the actors of the 80's classic "The Princess Bride". Query variable `pbActors` matches the UIDs of all actors from the movie. Value variable `roles` is thus a map from actor UID to number of roles. Value variable `roles` can be used in the `totalRoles` query block because that query block also matches the `pbActors` UIDs, so the actor to number of roles map is available. @@ -132,4 +132,4 @@ Query Example: Each actor who has been in a Peter Jackson movie and the fraction } {{< /runnable >}} -More examples can be found in two Dgraph blog posts about using variable propagation for recommendation engines ([post 1](https://open.dgraph.io/post/recommendation/), [post 2](https://open.dgraph.io/post/recommendation2/)). \ No newline at end of file +More examples can be found in two Dgraph blog posts about using variable propagation for recommendation engines ([post 1](https://open.dgraph.io/post/recommendation/), [post 2](https://open.dgraph.io/post/recommendation2/)). diff --git a/wiki/content/slash-graphql/_index.md b/wiki/content/slash-graphql/_index.md new file mode 100644 index 00000000000..800347f47f7 --- /dev/null +++ b/wiki/content/slash-graphql/_index.md @@ -0,0 +1,7 @@ ++++ +title = "Slash GraphQL" +[menu.main] + url = "/slash-graphql/" + identifier = "slash-graphql" + weight = 4 ++++ \ No newline at end of file diff --git a/wiki/content/slash-graphql/admin/_index.md b/wiki/content/slash-graphql/admin/_index.md new file mode 100644 index 00000000000..f01efdfba3c --- /dev/null +++ b/wiki/content/slash-graphql/admin/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Administering Your Backend" +[menu.main] + url = "/slash-graphql/admin/" + identifier = "slash-graphql-admin" + parent = "slash-graphql" + weight = 15 ++++ diff --git a/wiki/content/slash-graphql/admin/authentication.md b/wiki/content/slash-graphql/admin/authentication.md new file mode 100644 index 00000000000..24c54b4f687 --- /dev/null +++ b/wiki/content/slash-graphql/admin/authentication.md @@ -0,0 +1,17 @@ ++++ +title = "Authentication" +[menu.main] + parent = "slash-graphql-admin" + weight = 2 ++++ + +All the APIs documented here require an API token for access. A new API token can be generated from Slash GraphQL by selecting the ["Settings" button](https://slash.dgraph.io/_/settings) from the sidebar, then clicking the Add API Key button. Keep your API key safe, it will not be accessible once you leave the page. + +All admin API requests must be authenticated by passing the API token as the 'X-Auth-Token' header to every HTTP request. You can verify that your API token works by using the following HTTP example. + +``` +curl 'https:///admin' \ + -H 'X-Auth-Token: ' \ + -H 'Content-Type: application/json' \ + --data-binary '{"query":"{ getGQLSchema { schema } }"}' +``` diff --git a/wiki/content/slash-graphql/admin/backend-modes.md b/wiki/content/slash-graphql/admin/backend-modes.md new file mode 100644 index 00000000000..9eb3f124494 --- /dev/null +++ b/wiki/content/slash-graphql/admin/backend-modes.md @@ -0,0 +1,26 @@ ++++ +title = "Switching Backend Modes" +[menu.main] + parent = "slash-graphql-admin" + weight = 6 ++++ + +Slash GraphQL supports different 3 different backend modes, which controls how the underlying Dgraph instance is configured + +### Readonly Mode + +In readonly mode, only queries are allowed. All mutations and attempts to alter schema will be disallowed. + +### GraphQL Mode + +GraphQL mode is the default setting on Slash GraphQL, and is suitable for backends where the primary mode of interaction is via the GraphQL APIs. You can use of DQL/GraphQL+- queries and mutations, as described in the [advanced queries](/slash-graphql/advanced-queries/) section. However, all queries and mutations must be valid as per the applied GraphQL schema. + +### Flexible Mode + +Flexible mode is suitable for users who are already familiar with Dgraph, and intent to interact with their backend with DQL/GraphQL+-. Flexible mode removes any restrictions on queries and mutations, and also provides users access to advanced Dgraph features like directly altering the schema with the `/alter` http and GRPC endpoints. + +Running your backend in flexible mode is also a requirement for upcoming features such as support for Dgraph's ACL. + +## Changing your Backend Mode + +You can change the backend mode on the [settings page](https://slash.dgraph.io/_/settings), under the "Advanced" tab. diff --git a/wiki/content/slash-graphql/admin/drop-data.md b/wiki/content/slash-graphql/admin/drop-data.md new file mode 100644 index 00000000000..140b326a798 --- /dev/null +++ b/wiki/content/slash-graphql/admin/drop-data.md @@ -0,0 +1,34 @@ ++++ +title = "Dropping Data from your Backend" +[menu.main] + parent = "slash-graphql-admin" + weight = 5 ++++ + +It is possible to drop all data from your Slash GraphQL backend, and start afresh while retaining the same endpoint. Be careful, as this operation is not reversible, and all data will be lost. It is highly recommended that you [export](/slash-graphql/admin/import-export) your data before you drop your data. + +In order to drop all data while retaining the schema, please click the `Drop Data` button under the [Settings](https://slash.dgraph.io/_/settings) tab in the sidebar. + +### Dropping Data Programatically + +In order to do this, call the `dropData` mutation on `/admin/slash`. As an example, if your graphql endpoint is `https://frozen-mango-42.us-west-2.aws.cloud.dgraph.io/graphql`, then the admin endpoint for schema will be at `https://frozen-mango.us-west-2.aws.cloud.dgraph.io/admin/slash`. + +Please note that this endpoint requires [Authentication](/slash-graphql/admin/authentication). + +Please see the following curl as an example. + +``` +curl 'https:///admin/slash' \ + -H 'X-Auth-Token: ' \ + -H 'Content-Type: application/graphql' \ + --data-binary 'mutation { dropData(allData: true) { response { code message } } }' +``` + +If you would like to drop the schema along with the data, then you can set the `allDataAndSchema` flag. + +``` +curl 'https:///admin/slash' \ + -H 'X-Auth-Token: ' \ + -H 'Content-Type: application/graphql' \ + --data-binary 'mutation { dropData(allDataAndSchema: true) { response { code message } } }' +``` diff --git a/wiki/content/slash-graphql/admin/import-export.md b/wiki/content/slash-graphql/admin/import-export.md new file mode 100644 index 00000000000..dc4036c2914 --- /dev/null +++ b/wiki/content/slash-graphql/admin/import-export.md @@ -0,0 +1,45 @@ ++++ +title = "Importing and Exporting data from Slash GraphQL" +[menu.main] + parent = "slash-graphql-admin" + weight = 4 ++++ + +It is possible to export your data from one slash backend, and then import this data back into another Dgraph instance or Slash Backend. + +## Exporting Data + +It is possible to export your data via a JSON format. In order to do this, call the `export` mutation on `/admin/slash`. As an example, if your graphql endpoint is `https://frozen-mango-42.us-west-2.aws.cloud.dgraph.io/graphql`, then the admin endpoint for schema will be at `https://frozen-mango.us-west-2.aws.cloud.dgraph.io/admin/slash`. + +Please note that this endpoint requires [Authentication](/slash-graphql/admin/authentication). + +Below is a sample GraphQL body to export data to JSON. + +```graphql +mutation { + export { + response { code message } + signedUrls + } +} +``` + +The `signedUrls` output field contains a list of URLs which can be downloaded. The URLs will expire after 48 hours. + +Export will usually return 3 files: +* g01.gql_schema.gz - The GraphQL schema file. This file can be reimported via the [Schema APIs](/slash-graphql/admin/schema) +* g01.json.gz - the data from your instance, which can be imported via live loader +* g01.schema.gz - This file is the internal Dgraph schema. If you have set up your backend with a GraphQL schema, then you should be able to ignore this file. + +## Importing data with Live Loader + +It is possible to import data into a Slash GraphQL backend using [live loader](https://dgraph.io/docs/deploy/#live-loader). In order to import data, do the following steps + +1. First import your schema into your Slash GraphQL backend, using either the [Schema API](/slash-graphql/admin/schema) or via [the Schema Page](https://slash.dgraph.io/_/schema). +2. Find the gRPC endpoint for your cluster, as described in the [advanced queries](/slash-graphql/advanced-queries) section. This will look like frozen-mango-42.grpc.us-west-1.aws.cloud.dgraph.io:443 +3. Run the live loader as follows. Do note that running this via docker requires you to use an unreleased tag (either master or v20.07-slash) + +``` +docker run -it --rm -v /path/to/g01.json.gz:/tmp/g01.json.gz dgraph/dgraph:v20.07-slash \ + dgraph live --slash_grpc_endpoint=:443 -f /tmp/g01.json.gz -t +``` diff --git a/wiki/content/slash-graphql/admin/overview.md b/wiki/content/slash-graphql/admin/overview.md new file mode 100644 index 00000000000..fa34e3eca8c --- /dev/null +++ b/wiki/content/slash-graphql/admin/overview.md @@ -0,0 +1,22 @@ ++++ +date = "2017-03-20T22:25:17+11:00" +title = "Overview" +[menu.main] + parent = "slash-graphql-admin" + name = "Overview" + identifier = "slash-overview" + weight = 1 ++++ + +*These are draft docs for Slash GraphQL, which is currently in beta* + +Here is a guide to programatically administering your Slash GraphQL backend. + +Wherever possible, we have maintained compatibility with the corresponding Dgraph API, with the additional step of requiring authentication via the 'X-Auth-Token' header. + +Please see the following topics: + +* [Authentication](/slash-graphql/admin/authentication) will guide you in creating a API token. Since all admin APIs require an auth token, this is a good place to start. +* [Schema](/slash-graphql/admin/schema) describes how to programatically query and update your GraphQL schema. +* [Import and Exporting Data](/slash-graphql/admin/import-export) is a guide for exporting your data from a Slash GraphQL backend, and how to import it into another cluster +* [Dropping Data](/slash-graphql/admin/drop-data) will guide you through dropping all data from your Slash GraphQL backend. diff --git a/wiki/content/slash-graphql/admin/schema.md b/wiki/content/slash-graphql/admin/schema.md new file mode 100644 index 00000000000..3d79cf8b0c4 --- /dev/null +++ b/wiki/content/slash-graphql/admin/schema.md @@ -0,0 +1,38 @@ ++++ +title = "Fetching and Updating Your Schema" +[menu.main] + parent = "slash-graphql-admin" + weight = 3 ++++ + +Your GraphQL schema can be fetched and updated using the `/admin` endpoint of your cluster. As an example, if your graphql endpoint is `https://frozen-mango-42.us-west-2.aws.cloud.dgraph.io/graphql`, then the admin endpoint for schema will be at `https://frozen-mango.us-west-2.aws.cloud.dgraph.io/admin`. + +This endpoint works in a similar way to the [/admin](/graphql/admin) endpoint of Dgraph, with the additional constraint of [requiring authentication](/slash-graphql/admin/authentication). + +### Fetching the Current Schema + +It is possible to fetch your current schema using the `getGQLSchema` query on `/admin`. Below is a sample GraphQL query which will fetch this schema. + +```graphql +{ + getGQLSchema { + schema + } +} +``` + +### Setting a New Schema + +You can save a new schema using the `updateGQLSchema` mutation on `/admin`. Below is an example GraphQL body, with a variable called sch which must be passed in as a [variable](https://graphql.org/graphql-js/passing-arguments/) + +```graphql +mutation($sch: String!) { + updateGQLSchema(input: { set: { schema: $sch}}) + { + gqlSchema { + schema + generatedSchema + } + } +} +``` diff --git a/wiki/content/slash-graphql/advanced-queries.md b/wiki/content/slash-graphql/advanced-queries.md new file mode 100644 index 00000000000..402d7adb868 --- /dev/null +++ b/wiki/content/slash-graphql/advanced-queries.md @@ -0,0 +1,144 @@ ++++ +title = "Advanced Queries with GraphQL+-" +[menu.main] + parent = "slash-graphql" + weight = 2 ++++ + +*It is now possible to [embed GraphQL+- queries inside your GraphQL schema](/graphql/custom/graphqlpm), which is recommended for most use cases. The rest of this document covers how to connect to connect to your Slash GraphQL backend with existing Dgraph clients.* + +Slash GraphQL also supports running advanced queries with `GraphQL+-`, a query language that is unique to Dgraph. GraphQL+- should be used by advanced users who wish to make queries and mutations using existing Dgraph client libraries, either via the HTTP or gRPC endpoints. You can learn more about existing client libraries by following this [documentation](https://dgraph.io/docs/clients/). + +If you are getting started with Slash GraphQL, you might want to consider using our [GraphQL APIs](/graphql/overview) instead. It will get you quickly started on the basics of using Slash GraphQL before you go into advanced topics. + +Please note that Slash GraphQL does not allow you to alter the schema or create new predicates via GraphQL+-. You will also not be able ta access the /alter endpoint or it's gRPC equivalent. Please add your schema through the GraphQL endpoint (either via the UI or via the Admin API), before accessing the data with GraphQL+-. + +## Authentication + +All the APIs documented here require an API token for access. Please see [Authentication](/slash-graphql/admin/authentication) if you would like to create a new API token. + +### HTTP + +You can query your backend with GraphQL+- using the `/query` endpoint of your cluster. As an example, if your graphql endpoint is `https://frozen-mango-42.us-west-2.aws.cloud.dgraph.io/graphql`, then the admin endpoint for schema will be at `https://frozen-mango.us-west-2.aws.cloud.dgraph.io/query`. + +This endpoint works identically to to the [/query](https://dgraph.io/docs/query-language/) endpoint of Dgraph, with the additional constraint of requiring authentication, as described in the Authentication section above. + +You may also access the [`/mutate`](https://dgraph.io/docs/mutations/) and `/commit` endpoints. + +For the given GraphQL Schema: +```graphql +type Person { + name: String! @search(by: [fulltext]) + age: Int + country: String +} +``` + +Here is an example of a cURL for `/mutate` endpoint: +``` +curl -H "Content-Type: application/rdf" -H "x-auth-token: " -X POST "/mutate?commitNow=true" -d $' +{ + set { + _:x "John" . + _:x "30" . + _:x "US" . + } +}' +``` +Here is an example of a cURL for `/query` endpoint: +``` +curl -H "Content-Type: application/graphql+-" -H "x-auth-token: " -XPOST "/query" -d '{ + queryPerson(func: type(Person)) { + Person.name + Person.age + Person.country + } +}' +``` + +### gRPC + +The gRPC endpoint works identically to Dgraph's gRPC endpoint, with the additional constraint of requiring authentication on every gRPC call. The Slash API token must be passed in the "authorization" metadata to every gRPC call. This may be achieved by using [Metadata Call Credentials](https://godoc.org/google.golang.org/grpc/credentials#PerRPCCredentials) or the equivalent in your language. + +For example, if your GraphQL Endpoint is `https://frozen-mango-42.eu-central-1.aws.cloud.dgraph.io/graphql`, your gRPC endpoint will be `frozen-mango-42.grpc.eu-central-1.aws.cloud.dgraph.io:443`. + +Here is an example which uses the [pydgraph client](https://github.com/dgraph-io/pydgraph) to make gRPC requests. + +For initial setup, make sure you import the right packages and setup your `HOST` and `PORT` correctly. + +```python +import grpc +import sys +import json +from operator import itemgetter + +import pydgraph + + +GRPC_HOST = "frozen-mango-42.grpc.eu-central-1.aws.cloud.dgraph.io" +GRPC_PORT = "443" +``` + +You will then need to pass your API key as follows: +```python +creds = grpc.ssl_channel_credentials() +call_credentials = grpc.metadata_call_credentials(lambda context, callback: callback((("authorization", ""),), None)) +composite_credentials = grpc.composite_channel_credentials(creds, call_credentials) +client_stub = pydgraph.DgraphClientStub('{host}:{port}'.format(host=GRPC_HOST, port=GRPC_PORT), composite_credentials) +client = pydgraph.DgraphClient(client_stub) +``` + +For mutations, you can use the following example: +```python +mut = { + "Person.name": "John Doe", + "Person.age": "32", + "Person.country": "US" +} + +txn = client.txn() +try: + res = txn.mutate(set_obj=mut) + print(ppl) +finally: + txn.discard() +``` + +And for a query you can use the following example: +```python +query = """ +{ + queryPerson(func: type(Person)) { + Person.name + Person.age + Person.country + } +}""" +txn = client.txn() +try: + res = txn.query(query) + ppl = json.loads(res.json) + print(ppl) +finally: + txn.discard() +``` + +### Visualizing your Graph with Ratel + +It is possible to use Ratel to visualize your Slash GraphQL backend with GraphQL+-. You may use self hosted Ratel, or using [Dgraph Play](https://play.dgraph.io/?latest#connection) + +In order to configure Ratel, please do the following: + +* Click the Dgraph logo in the top left to bring up the connection screen (by default, it has the caption: play.dgraph.io) +* Enter your backend's host in the Dgraph Server URL field. This is obtained by removing `/graphql` from the end of your graphql endpoint. As an example, if your graphql endpoint is `https://frozen-mango-42.us-west-2.aws.cloud.dgraph.io/graphql`, then the host for ratel will be at `https://frozen-mango.us-west-2.aws.cloud.dgraph.io` +* Click the blue 'Connect' button. You should see a green tick check next to the word connected +* Click on the 'Extra Settings' tab, and enter your API token into the 'Slash API Key" field. Please see [Authentication](/slash-graphql/admin/authentication) if you would like to create a new API token. +* Click on the blue 'Continue' button + +You may now queries and mutation via Ratel, and see visualizations of your data. + +However, please note that certain functionality will not work, such as running Backups, modifying ACL or attempting to remove nodes from the cluster. + +### Switching Backend Modes + +For those who are interested in using DQL/GraphQL+- as their primary mode of interaction with the backend, it is possible to switch your backend to flexible mode. Please see [Backend Modes](/slash-graphql/admin/backend-modes) diff --git a/wiki/content/slash-graphql/introduction.md b/wiki/content/slash-graphql/introduction.md new file mode 100644 index 00000000000..0affc460c13 --- /dev/null +++ b/wiki/content/slash-graphql/introduction.md @@ -0,0 +1,25 @@ ++++ +title = "Introduction" +[menu.main] + parent = "slash-graphql" + weight = 1 ++++ + +

Slash GraphQL Provides /graphql Backend for Your App

+ +Please see the following topics: + +- The [QuickStart](/slash-graphql/slash-quick-start) will help you get started with a Slash GraphQL Schema, starting with a multi tenant todo app +- [Administering your Backend](/slash-graphql/admin/overview) covers topics such as how to programatically set your schema, and import or export your data + - [Authentication](/slash-graphql/admin/authentication) will guide you in creating a API token. Since all admin APIs require an auth token, this is a good place to start. + - [Schema](/slash-graphql/admin/schema) describes how to programatically query and update your GraphQL schema. + - [Import and Exporting Data](/slash-graphql/admin/import-export) is a guide for exporting your data from a Slash GraphQL backend, and how to import it into another cluster + - [Dropping Data](/slash-graphql/admin/drop-data) will guide you through dropping all data from your Slash GraphQL backend. + - [Switching Backend Modes](/slash-graphql/admin/backend-modes) will guide you through changing Slash GraphQL backend mode. +- [Advanced Queries With GraphQL+-](/slash-graphql/advanced-queries) speaks about how to interact with your database via the gRPC endpoint. +- [One-click Deploy](/slash-graphql/one-click-deploy) speaks about how to deploy sample apps in a fresh instance of backend to start working with them. + +You might also be interested in: + +- [Dgraph GraphQL Schema Reference](/graphql/schema/schema-overview), which lists all the types and directives supported by Dgraph +- [Dgraph GraphQL API Reference](/graphql/api/api-overview), which serves as a guide to using your new `/graphql` endpoint diff --git a/wiki/content/slash-graphql/one-click-deploy.md b/wiki/content/slash-graphql/one-click-deploy.md new file mode 100644 index 00000000000..63b873923b4 --- /dev/null +++ b/wiki/content/slash-graphql/one-click-deploy.md @@ -0,0 +1,25 @@ ++++ +title = "One-click Deploy" +[menu.main] + parent = "slash-graphql" + weight = 2 ++++ + +We love building GraphQL apps at Dgraph! In order to help you get started quickly, we are making it easy to launch a few apps with a single click. We'll create a fresh backend for you, load it with the right schema, and even launch a front end that's ready for you to use. + +In order to do a `One-Click Deploy`, please choose the application you wish to deploy from the [Apps](https://slash.dgraph.io/_/one-click) tab in the side bar and click the `Deploy` button. + +It will take few minutes while the new back-end spins up. You will be able to see the front-end url under `Details` card on the [Dashboard](https://slash.dgraph.io/_/dashboard) tab in the sidebar. + +## Deploying to your own domain. + +If you wish to deploy your apps to your own domain, you can do that easily with any of the hosting services. You can follow below steps to deploy your app on [Netlify](https://www.netlify.com/). + +1. Fork the github repo of the app you wish to deploy. +2. Link your forked repo to Netlify by Clicking `New Site from Git` and + {{% load-img "/images/importSite.png" "Import Site" %}} +3. After successful import of the forked repository click on `Show advanced` and `New variable`. +4. Add `REACT_APP_GRAPHQL_ENDPOINT` in the key and your graphql endpoint obtained from Slash GraphQL in the value field. +5. Click `Deploy Site` on Netlify Dashboard. + {{% load-img "/images/advanced-SettingsNetlify.png" "Advanced Settings" %}} +6. You can configure [Auth0](https://auth0.com/) steps by following Authorization section of the blogpost [here.](https://dgraph.io/blog/post/surveyo-into/) diff --git a/wiki/content/slash-graphql/security.md b/wiki/content/slash-graphql/security.md new file mode 100644 index 00000000000..9328dae6ab0 --- /dev/null +++ b/wiki/content/slash-graphql/security.md @@ -0,0 +1,20 @@ ++++ +title = "Securing Your GraphQL endpoint" +[menu.main] + parent = "slash-graphql" + weight = 6 ++++ + +Here are a few tips for securing your Slash GraphQL Backend + +### Writing Auth Rules + +All GraphQL queries and mutations are unrestricted by default. In order to restrict access, please see the [the @auth directive](https://dgraph.io/docs/graphql/authorization/directive/). + +### Restricting CORS from allowlisted domains + +Restricting the origins that your Slash GraphQL responds to is a an important step in preventing XSS exploits. Your Slash GraphQL backend will prevent any origins that are not in the allowlist from accessing your GraphQL endpoint. + +In order to add origins to the allow list, please see the [settings page](https://slash.dgraph.io/_/settings), under the "CORS" tab. By default, we allow all origins to connect to your endpoint (`Access-Control-Allow-Origin: *`), and adding an origin will prevent this default behavior. On adding your first origin, we automatically add "https://slash.dgraph.io" as well, so that the API explorer continues to work. + +Note: CORS restrictions are not a replacement for writing auth rules, as it is possible for malicious actors to bypass these restrictions. Also note that the CORS restrictions only applies to diff --git a/wiki/content/slash-graphql/slash-quick-start.md b/wiki/content/slash-graphql/slash-quick-start.md new file mode 100644 index 00000000000..c671bad6049 --- /dev/null +++ b/wiki/content/slash-graphql/slash-quick-start.md @@ -0,0 +1,141 @@ ++++ +title = "Slash Quick Start" +[menu.main] + parent = "slash-graphql" + weight = 1 ++++ + +*These are draft docs for Slash GraphQL, which is currently in beta* + +Welcome to Slash GraphQL. By now, you should have created your first deployment, and are looking for a schema to test out. Don't worry, we've got you covered. + +This example is for todo app that can support multiple users. We just have two types: Tasks and Users. + +Here's a schema that works with Slash GraphQL: + +```graphql +type Task { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} + +type User { + username: String! @id @search(by: [hash]) + name: String @search(by: [exact]) + tasks: [Task] @hasInverse(field: user) +} +``` + +Let's paste that into the schema tab of Slash GraphQL and hit submit. You now have a fully functional GraphQL API that allows you to create, query and modify records of these two types. + +No, really, that's all; nothing else to do; it's there, serving GraphQL --- let's go use it. + +## The Schema + +The schema itself was pretty simple. It was just a standard GraphQL schema, with a few directives (like `@search`), which are specific to Slash GraphQL. + +The task type has four fields: id, title, completed and the user. The title has the `@search` directive on it, which tells Slash Graphql that this field can be used in full text search queries. + +The User type uses the username field as an ID, and we will put the email address into that field. + +Let's go ahead and populate some data into this fresh database. + +## GraphQL Mutations + +If you head over to the API explorer tab, you should see the docs tab, which tells you the queries and mutations that your new database supports. Lets create a bunch of tasks, for a few of our users + +```graphql +mutation AddTasks { + addTask(input: [ + {title: "Create a database", completed: false, user: {username: "your-email@example.com"}}, + {title: "Write A Schema", completed: false, user: {username: "your-email@example.com"}}, + {title: "Put Data In", completed: false, user: {username: "your-email@example.com"}}, + {title: "Complete Tasks with UI", completed: false, user: {username: "your-email@example.com"}}, + {title: "Profit!", completed: false, user: {username: "your-email@example.com"}}, + + {title: "Walking", completed: false, user: {username: "frodo@dgraph.io"}}, + {title: "More Walking", completed: false, user: {username: "frodo@dgraph.io"}}, + {title: "Discard Jewelery", completed: false, user: {username: "frodo@dgraph.io"}}, + + {title: "Meet Dad", completed: false, user: {username: "skywalker@dgraph.io"}}, + {title: "Dismantle Empire", completed: false, user: {username: "skywalker@dgraph.io"}} + ]) { + numUids + task { + title + user { + username + } + } + } +} +``` + +Let's also query back the users and their tasks +```graphql +{ + queryUser { + username, + tasks { + title + } + } +} +``` + +You'll see that Slash figured out that users are unique by their username, and so you only see a single record for each user. + +## Auth + +Now that we have a schema working, let's update that schema to add some authorization. We'll update the schema so that users can only read their own tasks back. + +```graphql +type Task @auth( + query: { rule: """ + query($USER: String!) { + queryTask { + user(filter: { username: { eq: $USER } }) { + __typename + } + } + }"""}), { + id: ID! + title: String! @search(by: [fulltext]) + completed: Boolean! @search + user: User! +} + +type User { + username: String! @id @search(by: [hash]) + name: String @search(by: [exact]) + tasks: [Task] @hasInverse(field: user) +} + +# Dgraph.Authorization {"Header":"X-Auth-Token","Namespace":"https://dgraph.io/jwt/claims","Algo":"RS256","Audience":["Q1nC2kLsN6KQTX1UPdiBS6AhXRx9KwKl"],"VerificationKey":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp/qw/KXH23bpOuhXzsDp\ndo9bGNqjd/OkH2LkCT0PKFx5i/lmvFXdd04fhJD0Z0K3pUe7xHcRn1pIbZWlhwOR\n7siaCh9L729OQjnrxU/aPOKwsD19YmLWwTeVpE7vhDejhnRaJ7Pz8GImX/z/Xo50\nPFSYdX28Fb3kssfo+cMBz2+7h1prKeLZyDk30ItK9MMj9S5y+UKHDwfLV/ZHSd8m\nVVEYRXUNNzLsxD2XaEC5ym2gCjEP1QTgago0iw3Bm2rNAMBePgo4OMgYjH9wOOuS\nVnyvHhZdwiZAd1XtJSehORzpErgDuV2ym3mw1G9mrDXDzX9vr5l5CuBc3BjnvcFC\nFwIDAQAB\n-----END PUBLIC KEY-----"} +``` + +Slash GraphQL allows you to pass JWT with custom claims as a header, and will apply rules to control who can query or modify the data in your database. The `@auth` directive controls how these rules are applied, as filters that are generated from the JWT token. + +In our schema, we specify that one can only query tasks if the tasks's user has a username that matches `$USER`, a field in the JWT token. + +The Authorization magic comment specifies the header the JWT comes from, the domain, and the key that's signed it. In this example, the key is tied to our dev Auth0 account. + +More information on how this works in [the documentation](/graphql/authorization/authorization-overview). + +Let's try querying back the tasks. We should be getting empty results here, since you no longer have access. + +```graphql +{ + queryTask { + title + } +} +``` + +## Testing it out with a Simple UI + +We've built a todo app with react that you can use to close these todos off. Let's head over to our sample react app, deployed at [https://relaxed-brahmagupta-f8020f.netlify.app/](https://relaxed-brahmagupta-f8020f.netlify.app/). + +You can try creating an account with your email, or logging in with frodo / skywalker. Like the first death star, Luke wasn't big on security, his password is `password`. Frodo has the same password. diff --git a/wiki/content/tips/index.md b/wiki/content/tips/index.md index 86792d64732..798945d3269 100644 --- a/wiki/content/tips/index.md +++ b/wiki/content/tips/index.md @@ -3,6 +3,7 @@ title = "GraphQL+-: Tips and Tricks" [menu.main] url = "/tips/" identifier = "tips" + parent = "dql" weight = 5 +++ diff --git a/wiki/content/tutorials/index.md b/wiki/content/tutorials/index.md index 056898a2879..67d1aca829d 100644 --- a/wiki/content/tutorials/index.md +++ b/wiki/content/tutorials/index.md @@ -4,6 +4,7 @@ title = "Tutorials - Get Started with Dgraph series" url = "/tutorials" name = "Tutorials" identifier = "tutorials" + parent = "dql" weight = 3 +++ diff --git a/wiki/scripts/build.sh b/wiki/scripts/build.sh index f9b858e109f..55a37a33bf1 100755 --- a/wiki/scripts/build.sh +++ b/wiki/scripts/build.sh @@ -18,8 +18,8 @@ PUBLIC="${PUBLIC:-public}" LOOP="${LOOP:-true}" # Binary of hugo command to run. HUGO="${HUGO:-hugo}" -OLD_THEME="old-theme" -NEW_THEME="master" +OLD_THEME="${OLD_THEME:-old-theme}" +NEW_THEME="${NEW_THEME:-master}" # TODO - Maybe get list of released versions from Github API and filter # those which have docs. @@ -86,8 +86,10 @@ rebuild() { export CURRENT_VERSION=${2} export VERSIONS=${VERSION_STRING} export DGRAPH_ENDPOINT=${DGRAPH_ENDPOINT:-"https://play.dgraph.io/query?latency=true"} + export CANONICAL_PATH="$HOST" HUGO_TITLE="Dgraph Doc ${2}"\ + CANONICAL_PATH=${HOST}\ VERSIONS=${VERSION_STRING}\ CURRENT_BRANCH=${1}\ CURRENT_VERSION=${2} ${HUGO} \ diff --git a/wiki/static/images/advanced-SettingsNetlify.png b/wiki/static/images/advanced-SettingsNetlify.png new file mode 100644 index 00000000000..f3012adb052 Binary files /dev/null and b/wiki/static/images/advanced-SettingsNetlify.png differ diff --git a/wiki/static/images/graphql/subscription_example.gif b/wiki/static/images/graphql/subscription_example.gif new file mode 100644 index 00000000000..2152a9d3a30 Binary files /dev/null and b/wiki/static/images/graphql/subscription_example.gif differ diff --git a/wiki/static/images/graphql/subscription_flow.png b/wiki/static/images/graphql/subscription_flow.png new file mode 100644 index 00000000000..9aaf9fbfce3 Binary files /dev/null and b/wiki/static/images/graphql/subscription_flow.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/certificate.png b/wiki/static/images/graphql/tutorial/todo/certificate.png new file mode 100644 index 00000000000..0469923f96e Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/certificate.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/dashboard.png b/wiki/static/images/graphql/tutorial/todo/dashboard.png new file mode 100644 index 00000000000..2ac9e3c59bc Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/dashboard.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/jwt.png b/wiki/static/images/graphql/tutorial/todo/jwt.png new file mode 100644 index 00000000000..8ec8e526e26 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/jwt.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/rule.png b/wiki/static/images/graphql/tutorial/todo/rule.png new file mode 100644 index 00000000000..0619d42e330 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/rule.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/slash-graphql-1.png b/wiki/static/images/graphql/tutorial/todo/slash-graphql-1.png new file mode 100644 index 00000000000..062f3ca7322 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/slash-graphql-1.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/slash-graphql-2.png b/wiki/static/images/graphql/tutorial/todo/slash-graphql-2.png new file mode 100644 index 00000000000..0c8796cafc2 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/slash-graphql-2.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/slash-graphql-3.png b/wiki/static/images/graphql/tutorial/todo/slash-graphql-3.png new file mode 100644 index 00000000000..6ef1a4dfbc9 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/slash-graphql-3.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/todo-graph-2.png b/wiki/static/images/graphql/tutorial/todo/todo-graph-2.png new file mode 100644 index 00000000000..fc86774b4cd Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/todo-graph-2.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/todo-graph.png b/wiki/static/images/graphql/tutorial/todo/todo-graph.png new file mode 100644 index 00000000000..fe771a1a382 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/todo-graph.png differ diff --git a/wiki/static/images/graphql/tutorial/todo/token.png b/wiki/static/images/graphql/tutorial/todo/token.png new file mode 100644 index 00000000000..c1a4a7d8659 Binary files /dev/null and b/wiki/static/images/graphql/tutorial/todo/token.png differ diff --git a/wiki/static/images/importSite.png b/wiki/static/images/importSite.png new file mode 100644 index 00000000000..f536d88cf43 Binary files /dev/null and b/wiki/static/images/importSite.png differ diff --git a/worker/backup_ee.go b/worker/backup_ee.go index f403b444189..1e8801d62af 100644 --- a/worker/backup_ee.go +++ b/worker/backup_ee.go @@ -51,6 +51,11 @@ func backupCurrentGroup(ctx context.Context, req *pb.BackupRequest) (*pb.Status, return nil, err } + closer, err := g.Node.startTask(opBackup) + if err != nil { + return nil, errors.Wrapf(err, "cannot start backup operation") + } + defer closer.Done() bp := NewBackupProcessor(pstore, req) return bp.WriteBackup(ctx) } diff --git a/worker/config.go b/worker/config.go index 4a5a8fad244..fe6c6dd91a2 100644 --- a/worker/config.go +++ b/worker/config.go @@ -39,14 +39,23 @@ const ( type Options struct { // PostingDir is the path to the directory storing the postings.. PostingDir string - // BadgerTables is the name of the mode used to load the badger tables. + // BadgerTables is the name of the mode used to load the badger tables for the p directory. BadgerTables string - // BadgerVlog is the name of the mode used to load the badger value log. + // BadgerVlog is the name of the mode used to load the badger value log for the p directory. BadgerVlog string - // BadgerCompressionLevel is the ZSTD compression level used by badger. A + // BadgerWalTables is the name of the mode used to load the badger tables for the w directory. + BadgerWalTables string + // BadgerWalVlog is the name of the mode used to load the badger value log for the w directory. + BadgerWalVlog string + + // WALDirCompressionLevel is the ZSTD compression level used by WAL directory. A + // higher value means more CPU intensive compression and better compression + // ratio. + WALDirCompressionLevel int + // PostingDirCompressionLevel is the ZSTD compression level used by Postings directory. A // higher value means more CPU intensive compression and better compression // ratio. - BadgerCompressionLevel int + PostingDirCompressionLevel int // WALDir is the path to the directory storing the write-ahead log. WALDir string // MutationsMode is the mode used to handle mutation requests. @@ -56,6 +65,15 @@ type Options struct { // AllottedMemory is the estimated size taken by the LRU cache. AllottedMemory float64 + // PBlockCacheSize is the size of block cache for pstore + PBlockCacheSize int64 + // PIndexCacheSize is the size of index cache for pstore + PIndexCacheSize int64 + // WBlockCacheSize is the size of block cache for wstore + WBlockCacheSize int64 + // WIndexCacheSize is the size of index cache for wstore + WIndexCacheSize int64 + // HmacSecret stores the secret used to sign JSON Web Tokens (JWT). HmacSecret x.SensitiveByteSlice // AccessJwtTtl is the TTL for the access JWT. diff --git a/worker/draft.go b/worker/draft.go index 59d906be699..ef9fdf65aab 100644 --- a/worker/draft.go +++ b/worker/draft.go @@ -38,7 +38,6 @@ import ( otrace "go.opencensus.io/trace" bpb "github.com/dgraph-io/badger/v2/pb" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgraph/conn" "github.com/dgraph-io/dgraph/dgraph/cmd/zero" "github.com/dgraph-io/dgraph/posting" @@ -47,6 +46,7 @@ import ( "github.com/dgraph-io/dgraph/schema" "github.com/dgraph-io/dgraph/types" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" ) type node struct { @@ -60,12 +60,12 @@ type node struct { applyCh chan []*pb.Proposal ctx context.Context gid uint32 - closer *y.Closer + closer *z.Closer streaming int32 // Used to avoid calculating snapshot // Used to track the ops going on in the system. - ops map[op]*y.Closer + ops map[op]*z.Closer opsLock sync.Mutex canCampaign bool @@ -86,6 +86,8 @@ func (id op) String() string { return "opIndexing" case opRestore: return "opRestore" + case opBackup: + return "opBackup" default: return "opUnknown" } @@ -96,6 +98,7 @@ const ( opSnapshot opIndexing opRestore + opBackup ) // startTask is used to check whether an op is already running. If a rollup is running, @@ -104,7 +107,7 @@ const ( // Restore operations have preference and cancel all other operations, not just rollups. // You should only call Done() on the returned closer. Calling other functions (such as // SignalAndWait) for closer could result in panics. For more details, see GitHub issue #5034. -func (n *node) startTask(id op) (*y.Closer, error) { +func (n *node) startTask(id op) (*z.Closer, error) { n.opsLock.Lock() defer n.opsLock.Unlock() @@ -123,7 +126,7 @@ func (n *node) startTask(id op) (*y.Closer, error) { } } - closer := y.NewCloser(1) + closer := z.NewCloser(1) switch id { case opRollup: if len(n.ops) > 0 { @@ -141,6 +144,17 @@ func (n *node) startTask(id op) (*y.Closer, error) { delete(n.ops, otherId) otherCloser.SignalAndWait() } + case opBackup: + // Backup cancels all other operations, except for other backups since + // only one restore operation should be active any given moment. + for otherId, otherCloser := range n.ops { + if otherId == opBackup { + return nil, errors.Errorf("another backup operation is already running") + } + // Remove from map and signal the closer to cancel the operation. + delete(n.ops, otherId) + otherCloser.SignalAndWait() + } case opSnapshot, opIndexing: for otherId, otherCloser := range n.ops { if otherId == opRollup { @@ -158,7 +172,7 @@ func (n *node) startTask(id op) (*y.Closer, error) { n.ops[id] = closer glog.Infof("Operation started with id: %s", id) - go func(id op, closer *y.Closer) { + go func(id op, closer *z.Closer) { closer.Wait() stopTask(id) }(id, closer) @@ -225,8 +239,8 @@ func newNode(store *raftwal.DiskStorage, gid uint32, id uint64, myAddr string) * // to maintain quorum health. applyCh: make(chan []*pb.Proposal, 1000), elog: trace.NewEventLog("Dgraph", "ApplyCh"), - closer: y.NewCloser(4), // Matches CLOSER:1 - ops: make(map[op]*y.Closer), + closer: z.NewCloser(4), // Matches CLOSER:1 + ops: make(map[op]*z.Closer), } if x.WorkerConfig.LudicrousMode { n.ex = newExecutor(&m.Applied) @@ -590,7 +604,7 @@ func (n *node) applyCommitted(proposal *pb.Proposal) error { defer x.UpdateDrainingMode(false) var err error - var closer *y.Closer + var closer *z.Closer closer, err = n.startTask(opRestore) if err != nil { return errors.Wrapf(err, "cannot start restore task") @@ -996,7 +1010,7 @@ func (n *node) Run() { go n.ReportRaftComms() if x.WorkerConfig.LudicrousMode { - closer := y.NewCloser(2) + closer := z.NewCloser(2) defer closer.SignalAndWait() go x.StoreSync(n.Store, closer) go x.StoreSync(pstore, closer) diff --git a/worker/executor.go b/worker/executor.go index 8e087878ab8..e88acb8b0e1 100644 --- a/worker/executor.go +++ b/worker/executor.go @@ -26,6 +26,8 @@ import ( "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgraph/posting" "github.com/dgraph-io/dgraph/protos/pb" + "github.com/dgraph-io/ristretto/z" + "github.com/golang/glog" ) @@ -42,14 +44,14 @@ type executor struct { sync.RWMutex predChan map[string]chan *subMutation - closer *y.Closer + closer *z.Closer applied *y.WaterMark } func newExecutor(applied *y.WaterMark) *executor { ex := &executor{ predChan: make(map[string]chan *subMutation), - closer: y.NewCloser(0), + closer: z.NewCloser(0), applied: applied, } go ex.shutdown() diff --git a/worker/file_handler.go b/worker/file_handler.go index 26d8959eeae..c2e6e442fdc 100644 --- a/worker/file_handler.go +++ b/worker/file_handler.go @@ -139,6 +139,11 @@ func (h *fileHandler) GetManifests(uri *url.URL, backupId string) ([]*Manifest, return nil, err } + // Sort manifests in the ascending order of their BackupNum so that the first + // manifest corresponds to the first full backup and so on. + sort.Slice(manifests, func(i, j int) bool { + return manifests[i].BackupNum < manifests[j].BackupNum + }) return manifests, nil } diff --git a/worker/groups.go b/worker/groups.go index 690c702c6c2..8b654cf99f9 100644 --- a/worker/groups.go +++ b/worker/groups.go @@ -27,7 +27,6 @@ import ( "github.com/dgraph-io/badger/v2" badgerpb "github.com/dgraph-io/badger/v2/pb" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgo/v200/protos/api" "github.com/dgraph-io/dgraph/conn" "github.com/dgraph-io/dgraph/ee/enc" @@ -35,6 +34,7 @@ import ( "github.com/dgraph-io/dgraph/raftwal" "github.com/dgraph-io/dgraph/schema" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" "github.com/golang/glog" "github.com/golang/protobuf/proto" "github.com/pkg/errors" @@ -42,16 +42,13 @@ import ( type groupi struct { x.SafeMutex - // TODO: Is this context being used? - ctx context.Context - cancel context.CancelFunc state *pb.MembershipState Node *node gid uint32 tablets map[string]*pb.Tablet triggerCh chan struct{} // Used to trigger membership sync blockDeletes *sync.Mutex // Ensure that deletion won't happen when move is going on. - closer *y.Closer + closer *z.Closer // Group checksum is used to determine if the tablets served by the groups have changed from // the membership information that the Alpha has. If so, Alpha cannot service a read. @@ -73,8 +70,6 @@ func groups() *groupi { // This function triggers RAFT nodes to be created, and is the entrance to the RAFT // world from main.go. func StartRaftNodes(walStore *badger.DB, bindall bool) { - gr.ctx, gr.cancel = context.WithCancel(context.Background()) - if x.WorkerConfig.MyAddr == "" { x.WorkerConfig.MyAddr = fmt.Sprintf("localhost:%d", workerPort()) } else { @@ -123,7 +118,7 @@ func StartRaftNodes(walStore *badger.DB, bindall bool) { continue } zc := pb.NewZeroClient(pl.Get()) - connState, err = zc.Connect(gr.ctx, m) + connState, err = zc.Connect(gr.Ctx(), m) if err == nil || x.ShouldCrash(err) { break } @@ -154,7 +149,7 @@ func StartRaftNodes(walStore *badger.DB, bindall bool) { x.UpdateHealthStatus(true) glog.Infof("Server is ready") - gr.closer = y.NewCloser(3) // Match CLOSER:1 in this file. + gr.closer = z.NewCloser(3) // Match CLOSER:1 in this file. go gr.sendMembershipUpdates() go gr.receiveMembershipUpdates() go gr.processOracleDeltaStream() @@ -164,6 +159,14 @@ func StartRaftNodes(walStore *badger.DB, bindall bool) { gr.proposeInitialTypes() } +func (g *groupi) Ctx() context.Context { + return g.closer.Ctx() +} + +func (g *groupi) IsClosed() bool { + return g.closer.Ctx().Err() != nil +} + func (g *groupi) informZeroAboutTablets() { // Before we start this Alpha, let's pick up all the predicates we have in our postings // directory, and ask Zero if we are allowed to serve it. Do this irrespective of whether @@ -202,7 +205,7 @@ func (g *groupi) proposeInitialTypes() { func (g *groupi) proposeInitialSchema() { initialSchema := schema.InitialSchema() - ctx := context.Background() + ctx := g.Ctx() for _, s := range initialSchema { if gid, err := g.BelongsToReadOnly(s.Predicate, 0); err != nil { glog.Errorf("Error getting tablet for predicate %s. Will force schema proposal.", @@ -227,7 +230,7 @@ func (g *groupi) upsertSchema(sch *pb.SchemaUpdate, typ *pb.TypeUpdate) { // Propose schema mutation. var m pb.Mutations // schema for a reserved predicate is not changed once set. - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(g.Ctx(), 10*time.Second) ts, err := Timestamps(ctx, &pb.Num{Val: 1}) cancel() if err != nil { @@ -246,7 +249,7 @@ func (g *groupi) upsertSchema(sch *pb.SchemaUpdate, typ *pb.TypeUpdate) { // This would propose the schema mutation and make sure some node serves this predicate // and has the schema defined above. for { - _, err := MutateOverNetwork(gr.ctx, &m) + _, err := MutateOverNetwork(gr.Ctx(), &m) if err == nil { break } @@ -363,8 +366,7 @@ func (g *groupi) applyState(state *pb.MembershipState) { } } - if err := g.Node.ProposePeerRemoval( - context.Background(), member.GetId()); err != nil { + if err := g.Node.ProposePeerRemoval(g.Ctx(), member.GetId()); err != nil { glog.Errorf("Error while proposing node removal: %+v", err) } }() @@ -431,7 +433,7 @@ func (g *groupi) BelongsToReadOnly(key string, ts uint64) (uint32, error) { Predicate: key, ReadOnly: true, } - out, err := zc.ShouldServe(context.Background(), tablet) + out, err := zc.ShouldServe(g.Ctx(), tablet) if err != nil { glog.Errorf("Error while ShouldServe grpc call %v", err) return 0, err @@ -463,7 +465,7 @@ func (g *groupi) sendTablet(tablet *pb.Tablet) (*pb.Tablet, error) { pl := g.connToZeroLeader() zc := pb.NewZeroClient(pl.Get()) - out, err := zc.ShouldServe(context.Background(), tablet) + out, err := zc.ShouldServe(g.Ctx(), tablet) if err != nil { glog.Errorf("Error while ShouldServe grpc call %v", err) return nil, err @@ -637,7 +639,7 @@ func (g *groupi) connToZeroLeader() *conn.Pool { glog.V(1).Infof("No healthy Zero leader found. Trying to find a Zero leader...") getLeaderConn := func(zc pb.ZeroClient) *conn.Pool { - ctx, cancel := context.WithTimeout(g.ctx, 10*time.Second) + ctx, cancel := context.WithTimeout(g.Ctx(), 10*time.Second) defer cancel() connState, err := zc.Connect(ctx, &pb.Member{ClusterInfoOnly: true}) @@ -657,6 +659,10 @@ func (g *groupi) connToZeroLeader() *conn.Pool { delay := connBaseDelay maxHalfDelay := time.Second for i := 0; ; i++ { // Keep on retrying. See: https://github.com/dgraph-io/dgraph/issues/2289 + if g.IsClosed() { + return nil + } + time.Sleep(delay) if delay <= maxHalfDelay { delay *= 2 @@ -709,7 +715,7 @@ func (g *groupi) doSendMembership(tablets map[string]*pb.Tablet) error { return errNoConnection } c := pb.NewZeroClient(pl.Get()) - ctx, cancel := context.WithTimeout(g.ctx, 10*time.Second) + ctx, cancel := context.WithTimeout(g.Ctx(), 10*time.Second) defer cancel() reply, err := c.UpdateMembership(ctx, group) if err != nil { @@ -724,7 +730,10 @@ func (g *groupi) doSendMembership(tablets map[string]*pb.Tablet) error { // sendMembershipUpdates sends the membership update to Zero leader. If this Alpha is the leader, it // would also calculate the tablet sizes and send them to Zero. func (g *groupi) sendMembershipUpdates() { - defer g.closer.Done() // CLOSER:1 + defer func() { + glog.Infoln("Closing sendMembershipUpdates") + g.closer.Done() // CLOSER:1 + }() ticker := time.NewTicker(time.Second) defer ticker.Stop() @@ -768,7 +777,10 @@ func (g *groupi) sendMembershipUpdates() { // connection which tells Alpha about the state of the cluster, including the latest Zero leader. // All the other connections to Zero, are only made only to the leader. func (g *groupi) receiveMembershipUpdates() { - defer g.closer.Done() // CLOSER:1 + defer func() { + glog.Infoln("Closing receiveMembershipUpdates") + g.closer.Done() // CLOSER:1 + }() ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() @@ -790,7 +802,7 @@ START: glog.Infof("Got address of a Zero leader: %s", pl.Addr) c := pb.NewZeroClient(pl.Get()) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(g.Ctx()) stream, err := c.StreamMembership(ctx, &api.Payload{}) if err != nil { cancel() @@ -864,7 +876,10 @@ OUTER: // processOracleDeltaStream is used to process oracle delta stream from Zero. // Zero sends information about aborted/committed transactions and maxPending. func (g *groupi) processOracleDeltaStream() { - defer g.closer.Done() // CLOSER:1 + defer func() { + glog.Infoln("Closing processOracleDeltaStream") + g.closer.Done() // CLOSER:1 + }() ticker := time.NewTicker(time.Second) defer ticker.Stop() @@ -876,6 +891,9 @@ func (g *groupi) processOracleDeltaStream() { pl := g.connToZeroLeader() if pl == nil { glog.Warningln("Oracle delta stream: No Zero leader known.") + if g.IsClosed() { + return + } time.Sleep(time.Second) return } @@ -886,8 +904,9 @@ func (g *groupi) processOracleDeltaStream() { // batching. Once a batch is created, it gets proposed. Thus, we can reduce the number of // times proposals happen, which is a great optimization to have (and a common one in our // code base). - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(g.Ctx()) defer cancel() + c := pb.NewZeroClient(pl.Get()) stream, err := c.Oracle(ctx, &api.Payload{}) if err != nil { @@ -1001,7 +1020,7 @@ func (g *groupi) processOracleDeltaStream() { for { // Block forever trying to propose this. Also this proposal should not be counted // towards num pending proposals and be proposed right away. - err := g.Node.proposeAndWait(context.Background(), &pb.Proposal{Delta: delta}) + err := g.Node.proposeAndWait(g.Ctx(), &pb.Proposal{Delta: delta}) if err == nil { break } @@ -1032,30 +1051,25 @@ func EnterpriseEnabled() bool { } g := groups() if g.state == nil { - return askZeroForEE() + return g.askZeroForEE() } g.RLock() defer g.RUnlock() return g.state.GetLicense().GetEnabled() } -func askZeroForEE() bool { +func (g *groupi) askZeroForEE() bool { var err error var connState *pb.ConnectionState - grp := &groupi{} - createConn := func() bool { - grp.ctx, grp.cancel = context.WithCancel(context.Background()) - defer grp.cancel() - - pl := grp.connToZeroLeader() + pl := g.connToZeroLeader() if pl == nil { return false } zc := pb.NewZeroClient(pl.Get()) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(g.Ctx(), 10*time.Second) defer cancel() connState, err = zc.Connect(ctx, &pb.Member{ClusterInfoOnly: true}) @@ -1071,7 +1085,7 @@ func askZeroForEE() bool { return false } - for { + for !g.IsClosed() { if createConn() { break } @@ -1081,43 +1095,51 @@ func askZeroForEE() bool { } // SubscribeForUpdates will listen for updates for the given group. -func SubscribeForUpdates(prefixes [][]byte, cb func(kvs *badgerpb.KVList), group uint32, - closer *y.Closer) { - defer closer.Done() +func SubscribeForUpdates(prefixes [][]byte, cb func(kvs *badgerpb.KVList), + group uint32, closer *z.Closer) { - for { - select { - case <-closer.HasBeenClosed(): - return - default: + var prefix []byte + if len(prefixes) > 0 { + prefix = prefixes[0] + } + defer func() { + glog.Infof("SubscribeForUpdates closing for prefix: %q\n", prefix) + closer.Done() + }() - // Connect to any of the group 1 nodes. - members := groups().AnyTwoServers(group) - // There may be a lag while starting so keep retrying. - if len(members) == 0 { - continue - } - pool := conn.GetPools().Connect(members[0]) - client := pb.NewWorkerClient(pool.Get()) + listen := func() error { + // Connect to any of the group 1 nodes. + members := groups().AnyTwoServers(group) + // There may be a lag while starting so keep retrying. + if len(members) == 0 { + return fmt.Errorf("Unable to find any servers for group: %d", group) + } + pool := conn.GetPools().Connect(members[0]) + client := pb.NewWorkerClient(pool.Get()) - // Get Subscriber stream. - stream, err := client.Subscribe(context.Background(), - &pb.SubscriptionRequest{Prefixes: prefixes}) + // Get Subscriber stream. + stream, err := client.Subscribe(closer.Ctx(), &pb.SubscriptionRequest{Prefixes: prefixes}) + if err != nil { + return errors.Wrapf(err, "error from client.subscribe") + } + for { + // Listen for updates. + kvs, err := stream.Recv() if err != nil { - glog.Errorf("Error from alpha client subscribe: %v", err) - time.Sleep(100 * time.Millisecond) - continue - } - receiver: - for { - // Listen for updates. - kvs, err := stream.Recv() - if err != nil { - glog.Errorf("Error from worker subscribe stream: %v", err) - break receiver - } - cb(kvs) + return errors.Wrapf(err, "while receiving from stream") } + cb(kvs) + } + } + + for { + if err := listen(); err != nil { + glog.Errorf("Error during SubscribeForUpdates for prefix %q: %v. closer err: %v\n", + prefix, err, closer.Ctx().Err()) } + if closer.Ctx().Err() != nil { + return + } + time.Sleep(time.Second) } } diff --git a/worker/mutation.go b/worker/mutation.go index 4bf39ec345b..0a958d7e215 100644 --- a/worker/mutation.go +++ b/worker/mutation.go @@ -29,7 +29,6 @@ import ( otrace "go.opencensus.io/trace" "github.com/dgraph-io/badger/v2" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgo/v200" "github.com/dgraph-io/dgo/v200/protos/api" "github.com/dgraph-io/dgraph/conn" @@ -38,6 +37,7 @@ import ( "github.com/dgraph-io/dgraph/schema" "github.com/dgraph-io/dgraph/types" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" ) var ( @@ -151,7 +151,7 @@ func runSchemaMutation(ctx context.Context, updates []*pb.SchemaUpdate, startTs // done is used to ensure that we only stop the indexing task once. var done uint32 start := time.Now() - stopIndexing := func(closer *y.Closer) { + stopIndexing := func(closer *z.Closer) { // runSchemaMutation can return. stopIndexing could be called by goroutines. if !schema.State().IndexingInProgress() { if atomic.CompareAndSwapUint32(&done, 0, 1) { diff --git a/worker/server_state.go b/worker/server_state.go index 9923d8d39f5..a9b9f5bce0e 100644 --- a/worker/server_state.go +++ b/worker/server_state.go @@ -24,9 +24,9 @@ import ( "github.com/dgraph-io/badger/v2" "github.com/dgraph-io/badger/v2/options" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgraph/protos/pb" "github.com/dgraph-io/dgraph/x" + "github.com/dgraph-io/ristretto/z" "github.com/golang/glog" ) @@ -36,7 +36,7 @@ type ServerState struct { Pstore *badger.DB WALstore *badger.DB - gcCloser *y.Closer // closer for valueLogGC + gcCloser *z.Closer // closer for valueLogGC needTs chan tsReq } @@ -62,7 +62,7 @@ func InitServerState() { x.WorkerConfig.ProposedGroupId = groupId } -func setBadgerOptions(opt badger.Options) badger.Options { +func setBadgerOptions(opt badger.Options, wal bool) badger.Options { opt = opt.WithSyncWrites(false).WithTruncate(true).WithLogger(&x.ToGlog{}). WithEncryptionKey(x.WorkerConfig.EncryptionKey) @@ -75,17 +75,38 @@ func setBadgerOptions(opt badger.Options) badger.Options { // saved by disabling it. opt.DetectConflicts = false - glog.Infof("Setting Badger Compression Level: %d", Config.BadgerCompressionLevel) - // Default value of badgerCompressionLevel is 3 so compression will always - // be enabled, unless it is explicitly disabled by setting the value to 0. - if Config.BadgerCompressionLevel != 0 { - // By default, compression is disabled in badger. - opt.Compression = options.ZSTD - opt.ZSTDCompressionLevel = Config.BadgerCompressionLevel + var badgerTables string + var badgerVlog string + if wal { + // Settings for the write-ahead log. + badgerTables = Config.BadgerWalTables + badgerVlog = Config.BadgerWalVlog + + glog.Infof("Setting WAL Dir Compression Level: %d", Config.WALDirCompressionLevel) + // Default value of WALDirCompressionLevel is 0 so compression will always + // be disabled, unless it is explicitly enabled by setting the value to greater than 0. + if Config.WALDirCompressionLevel != 0 { + // By default, compression is disabled in badger. + opt.Compression = options.ZSTD + opt.ZSTDCompressionLevel = Config.WALDirCompressionLevel + } + } else { + // Settings for the data directory. + badgerTables = Config.BadgerTables + badgerVlog = Config.BadgerVlog + + glog.Infof("Setting Posting Dir Compression Level: %d", Config.PostingDirCompressionLevel) + // Default value of postingDirCompressionLevel is 3 so compression will always + // be enabled, unless it is explicitly disabled by setting the value to 0. + if Config.PostingDirCompressionLevel != 0 { + // By default, compression is disabled in badger. + opt.Compression = options.ZSTD + opt.ZSTDCompressionLevel = Config.PostingDirCompressionLevel + } } glog.Infof("Setting Badger table load option: %s", Config.BadgerTables) - switch Config.BadgerTables { + switch badgerTables { case "mmap": opt.TableLoadingMode = options.MemoryMap case "ram": @@ -97,7 +118,7 @@ func setBadgerOptions(opt badger.Options) badger.Options { } glog.Infof("Setting Badger value log load option: %s", Config.BadgerVlog) - switch Config.BadgerVlog { + switch badgerVlog { case "mmap": opt.ValueLogLoadingMode = options.MemoryMap case "disk": @@ -126,16 +147,10 @@ func (s *ServerState) initStorage() { // Write Ahead Log directory x.Checkf(os.MkdirAll(Config.WALDir, 0700), "Error while creating WAL dir.") opt := badger.LSMOnlyOptions(Config.WALDir) - opt = setBadgerOptions(opt) + opt = setBadgerOptions(opt, true) opt.ValueLogMaxEntries = 10000 // Allow for easy space reclamation. - opt.MaxCacheSize = 10 << 20 // 10 mb of cache size for WAL. - - // We should always force load LSM tables to memory, disregarding user settings, because - // Raft.Advance hits the WAL many times. If the tables are not in memory, retrieval slows - // down way too much, causing cluster membership issues. Because of prefix compression and - // value separation provided by Badger, this is still better than using the memory based WAL - // storage provided by the Raft library. - opt.TableLoadingMode = options.LoadToRAM + opt.BlockCacheSize = Config.WBlockCacheSize + opt.IndexCacheSize = Config.WIndexCacheSize // Print the options w/o exposing key. // TODO: Build a stringify interface in Badger options, which is used to print nicely here. @@ -155,11 +170,9 @@ func (s *ServerState) initStorage() { opt := badger.DefaultOptions(Config.PostingDir). WithValueThreshold(1 << 10 /* 1KB */). WithNumVersionsToKeep(math.MaxInt32). - WithMaxCacheSize(1 << 30). - WithKeepBlockIndicesInCache(true). - WithKeepBlocksInCache(true). - WithMaxBfCacheSize(500 << 20) // 500 MB of bloom filter cache. - opt = setBadgerOptions(opt) + WithBlockCacheSize(Config.PBlockCacheSize). + WithIndexCacheSize(Config.PIndexCacheSize) + opt = setBadgerOptions(opt, false) // Print the options w/o exposing key. // TODO: Build a stringify interface in Badger options, which is used to print nicely here. @@ -175,7 +188,7 @@ func (s *ServerState) initStorage() { opt.EncryptionKey = nil } - s.gcCloser = y.NewCloser(2) + s.gcCloser = z.NewCloser(2) go x.RunVlogGC(s.Pstore, s.gcCloser) go x.RunVlogGC(s.WALstore, s.gcCloser) } @@ -203,20 +216,28 @@ func (s *ServerState) fillTimestampRequests() { maxDelay = time.Second ) + defer func() { + glog.Infoln("Exiting fillTimestampRequests") + }() + var reqs []tsReq for { // Reset variables. reqs = reqs[:0] delay := initDelay - req := <-s.needTs - slurpLoop: - for { - reqs = append(reqs, req) - select { - case req = <-s.needTs: - default: - break slurpLoop + select { + case <-s.gcCloser.HasBeenClosed(): + return + case req := <-s.needTs: + slurpLoop: + for { + reqs = append(reqs, req) + select { + case req = <-s.needTs: + default: + break slurpLoop + } } } @@ -232,7 +253,10 @@ func (s *ServerState) fillTimestampRequests() { // Execute the request with infinite retries. retry: - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + if s.gcCloser.Ctx().Err() != nil { + return + } + ctx, cancel := context.WithTimeout(s.gcCloser.Ctx(), 10*time.Second) ts, err := Timestamps(ctx, num) cancel() if err != nil { diff --git a/worker/task.go b/worker/task.go index 337775a3106..157b665293c 100644 --- a/worker/task.go +++ b/worker/task.go @@ -881,6 +881,9 @@ func processTask(ctx context.Context, q *pb.Query, gid uint32) (*pb.Result, erro if q.Cache == UseTxnCache { qs.cache = posting.Oracle().CacheAt(q.ReadTs) } + if qs.cache == nil { + qs.cache = posting.NoCache(q.ReadTs) + } // For now, remove the query level cache. It is causing contention for queries with high // fan-out. diff --git a/x/config.go b/x/config.go index b5ddd2416e5..5390720710a 100644 --- a/x/config.go +++ b/x/config.go @@ -34,6 +34,8 @@ type Options struct { PollInterval time.Duration //GraphqlExtension wiil be set to see extensions in graphql results GraphqlExtension bool + // GraphqlDebug will enable debug mode in GraphQL + GraphqlDebug bool } // Config stores the global instance of this package's options. diff --git a/x/sentry_integration.go b/x/sentry_integration.go index 63306066cbf..64375245c44 100644 --- a/x/sentry_integration.go +++ b/x/sentry_integration.go @@ -181,9 +181,17 @@ func WrapPanics() { if err != nil { panic(err) } - // If exitStatus >= 0, then we're the parent process and the panicwrap - // re-executed ourselves and completed. Just exit with the proper status. - if exitStatus >= 0 { + + // Note: panicwrap.Wrap documentation states that exitStatus == -1 + // should be used to determine whether the process is the child. + // However, this is not reliable. See https://github.com/mitchellh/panicwrap/issues/18 + // we have found that exitStatus = -1 is returned even when + // the process is the parent. Likely due to panicwrap returning + // syscall.WaitStatus.ExitStatus() as the exitStatus, which _can_ be + // -1. Checking panicwrap.Wrapped(nil) is more reliable. + if !panicwrap.Wrapped(nil) { + // parent os.Exit(exitStatus) } + // child } diff --git a/x/x.go b/x/x.go index 1394980dc0e..86a9a2f0cba 100644 --- a/x/x.go +++ b/x/x.go @@ -41,9 +41,9 @@ import ( "google.golang.org/grpc/peer" "github.com/dgraph-io/badger/v2" - "github.com/dgraph-io/badger/v2/y" "github.com/dgraph-io/dgo/v200" "github.com/dgraph-io/dgo/v200/protos/api" + "github.com/dgraph-io/ristretto/z" "github.com/golang/glog" "github.com/pkg/errors" @@ -997,7 +997,7 @@ func IsGuardian(groups []string) bool { // RunVlogGC runs value log gc on store. It runs GC unconditionally after every 10 minutes. // Additionally it also runs GC if vLogSize has grown more than 1 GB in last minute. -func RunVlogGC(store *badger.DB, closer *y.Closer) { +func RunVlogGC(store *badger.DB, closer *z.Closer) { defer closer.Done() // Get initial size on start. _, lastVlogSize := store.Size() @@ -1038,7 +1038,7 @@ type DB interface { Sync() error } -func StoreSync(db DB, closer *y.Closer) { +func StoreSync(db DB, closer *z.Closer) { defer closer.Done() ticker := time.NewTicker(1 * time.Second) for { @@ -1096,3 +1096,70 @@ func DeepCopyJsonArray(a []interface{}) []interface{} { } return aCopy } + +// GetCachePercentages returns the slice of cache percentages given the "," (comma) separated +// cache percentages(integers) string and expected number of caches. +func GetCachePercentages(cpString string, numExpected int) ([]int64, error) { + cp := strings.Split(cpString, ",") + // Sanity checks + if len(cp) != numExpected { + return nil, errors.Errorf("ERROR: expected %d cache percentages, got %d", + numExpected, len(cp)) + } + + var cachePercent []int64 + percentSum := 0 + for _, percent := range cp { + x, err := strconv.Atoi(percent) + if err != nil { + return nil, errors.Errorf("ERROR: unable to parse cache percentage(%s)", percent) + } + if x < 0 { + return nil, errors.Errorf("ERROR: cache percentage(%s) cannot be negative", percent) + } + cachePercent = append(cachePercent, int64(x)) + percentSum += x + } + + if percentSum != 100 { + return nil, errors.Errorf("ERROR: cache percentages (%s) does not sum up to 100", + strings.Join(cp, "+")) + } + + return cachePercent, nil +} + +// ParseCompressionLevel returns compression level(int) given the compression level(string) +func ParseCompressionLevel(compressionLevel string) (int, error) { + x, err := strconv.Atoi(compressionLevel) + if err != nil { + return 0, errors.Errorf("ERROR: unable to parse compression level(%s)", compressionLevel) + } + if x < 0 { + return 0, errors.Errorf("ERROR: compression level(%s) cannot be negative", compressionLevel) + } + return x, nil +} + +// GetCompressionLevels returns the slice of compression levels given the "," (comma) separated +// compression levels(integers) string. +func GetCompressionLevels(compressionLevelsString string) ([]int, error) { + compressionLevels := strings.Split(compressionLevelsString, ",") + // Validity checks + if len(compressionLevels) != 1 && len(compressionLevels) != 2 { + return nil, errors.Errorf("ERROR: expected single integer or two comma separated integers") + } + var compressionLevelsInt []int + for _, cLevel := range compressionLevels { + x, err := ParseCompressionLevel(cLevel) + if err != nil { + return nil, err + } + compressionLevelsInt = append(compressionLevelsInt, x) + } + // Append the same compression level in case only one level was passed. + if len(compressionLevelsInt) == 1 { + compressionLevelsInt = append(compressionLevelsInt, compressionLevelsInt[0]) + } + return compressionLevelsInt, nil +}