diff --git a/examples/federation/gateway/main.go b/examples/federation/gateway/main.go index 26550b43b1..11983c5ec9 100644 --- a/examples/federation/gateway/main.go +++ b/examples/federation/gateway/main.go @@ -18,7 +18,6 @@ import ( ) // It's just a simple example of graphql federation gateway server, it's NOT a production ready code. -// func logger() log.Logger { logger, err := zap.NewDevelopmentConfig().Build() if err != nil { diff --git a/examples/federation/go.sum b/examples/federation/go.sum index 84302683af..b4e23972c3 100644 --- a/examples/federation/go.sum +++ b/examples/federation/go.sum @@ -207,10 +207,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA= -github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= -github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0= -github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI= github.com/r3labs/sse/v2 v2.8.1 h1:lZH+W4XOLIq88U5MIHOsLec7+R62uhz3bIi2yn0Sg8o= github.com/r3labs/sse/v2 v2.8.1/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -308,7 +304,6 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -335,7 +330,6 @@ golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -358,7 +352,6 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/go.mod b/go.mod index cac0fb366a..238c4a6904 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,8 @@ module github.com/TykTechnologies/graphql-go-tools go 1.16 require ( - github.com/99designs/gqlgen v0.17.20 - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect + github.com/99designs/gqlgen v0.17.22 + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/Shopify/sarama v1.29.1 github.com/asyncapi/parser-go v0.4.2 @@ -20,9 +19,9 @@ require ( github.com/evanphx/json-patch/v5 v5.1.0 github.com/getkin/kin-openapi v0.89.0 github.com/go-test/deep v1.0.8 - github.com/go-zookeeper/zk v1.0.2 + github.com/go-zookeeper/zk v1.0.3 github.com/gobwas/ws v1.0.4 - github.com/golang/mock v1.4.1 + github.com/golang/mock v1.4.4 github.com/google/go-cmp v0.5.8 github.com/gorilla/websocket v1.5.0 github.com/gotestyourself/gotestyourself v1.4.0 // indirect @@ -40,8 +39,8 @@ require ( github.com/opencontainers/runc v1.1.2 // indirect github.com/ory/dockertest v3.3.5+incompatible github.com/pelletier/go-toml v1.6.0 // indirect - github.com/qri-io/jsonschema v0.2.1 github.com/r3labs/sse/v2 v2.8.1 + github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561 github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.3.2 @@ -51,7 +50,6 @@ require ( github.com/vektah/gqlparser/v2 v2.5.1 go.uber.org/atomic v1.9.0 go.uber.org/zap v1.18.1 - golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 gopkg.in/yaml.v2 v2.4.0 nhooyr.io/websocket v1.8.7 diff --git a/go.sum b/go.sum index be28bb202d..418c26bef2 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,12 @@ -github.com/99designs/gqlgen v0.17.20 h1:O7WzccIhKB1dm+7g6dhQcULINftfiLSBg2l/mwbpJMw= -github.com/99designs/gqlgen v0.17.20/go.mod h1:Mja2HI23kWT1VRH09hvWshFgOzKswpO20o4ScpJIES4= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/99designs/gqlgen v0.17.22 h1:TOcrF8t0T3I0za9JD3CB6ehq7dDEMjR9Onikf8Lc/04= +github.com/99designs/gqlgen v0.17.22/go.mod h1:BMhYIhe4bp7OlCo5I2PnowSK/Wimpv/YlxfNkqZGwLo= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= @@ -110,8 +109,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-zookeeper/zk v1.0.2 h1:4mx0EYENAdX/B/rbunjlt5+4RTA/a9SMHBRuSKdGxPM= -github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= +github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -121,8 +120,8 @@ github.com/gobwas/ws v1.0.4 h1:5eXU1CZhpQdq5kXbKb+sECH5Ia5KiO6CYzIzdlVx6Bs= github.com/gobwas/ws v1.0.4/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/mock v1.4.1 h1:ocYkMQY5RrXTYgXl7ICpV0IXwlEQGwKIsery4gyXa1U= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= @@ -234,8 +233,8 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA= -github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= @@ -291,10 +290,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA= -github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= -github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0= -github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI= github.com/r3labs/sse/v2 v2.8.1 h1:lZH+W4XOLIq88U5MIHOsLec7+R62uhz3bIi2yn0Sg8o= github.com/r3labs/sse/v2 v2.8.1/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= @@ -303,10 +298,11 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVPt6lD4/bhmzfiKo= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561 h1:IY+sDBJR/wRtsxq+626xJnt4Tw7/ROA9cDIR8MMhWyg= github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561/go.mod h1:lvjGftC8oe7XPtyrOidaMi0rp5B9+XY/ZRUynGnuaxQ= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -399,9 +395,8 @@ golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= @@ -466,13 +461,13 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -484,7 +479,6 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= @@ -530,5 +524,3 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/astvalidation/operation_rule_fragments.go b/pkg/astvalidation/operation_rule_fragments.go index 9bb9ad83f3..99906897d3 100644 --- a/pkg/astvalidation/operation_rule_fragments.go +++ b/pkg/astvalidation/operation_rule_fragments.go @@ -30,13 +30,24 @@ type fragmentsVisitor struct { } func (f *fragmentsVisitor) EnterFragmentSpread(ref int) { + fragmentName := f.operation.FragmentSpreadNameBytes(ref) + fragmentDefinitionRef, exists := f.operation.FragmentDefinitionRef(fragmentName) + if !exists { + f.StopWithExternalErr(operationreport.ErrFragmentUndefined(fragmentName)) + return + } + fragmentTypeName := f.operation.FragmentDefinitionTypeName(fragmentDefinitionRef) + enclosingTypeName := f.EnclosingTypeDefinition.NameBytes(f.definition) + if !bytes.Equal(fragmentTypeName, enclosingTypeName) { + f.StopWithExternalErr(operationreport.ErrInvalidFragmentSpread(fragmentName, fragmentTypeName, enclosingTypeName)) + return + } if f.Ancestors[0].Kind == ast.NodeKindOperationDefinition { - spreadName := f.operation.FragmentSpreadNameBytes(ref) - f.StopWithExternalErr(operationreport.ErrFragmentSpreadFormsCycle(spreadName)) + f.StopWithExternalErr(operationreport.ErrFragmentSpreadFormsCycle(fragmentName)) } } -func (f *fragmentsVisitor) LeaveDocument(operation, definition *ast.Document) { +func (f *fragmentsVisitor) LeaveDocument(_, _ *ast.Document) { for i := range f.fragmentDefinitionsVisited { if !f.operation.FragmentDefinitionIsUsed(f.fragmentDefinitionsVisited[i]) { fragmentName := f.fragmentDefinitionsVisited[i] diff --git a/pkg/astvalidation/operation_validation_test.go b/pkg/astvalidation/operation_validation_test.go index b88c2fbe6f..525b042aaa 100644 --- a/pkg/astvalidation/operation_validation_test.go +++ b/pkg/astvalidation/operation_validation_test.go @@ -2540,14 +2540,30 @@ func TestExecutionValidation(t *testing.T) { }) t.Run("5.5.2 Fragment Spreads", func(t *testing.T) { t.Run("5.5.2.1 Fragment spread target defined", func(t *testing.T) { - t.Run("133", func(t *testing.T) { + t.Run("Undefined fragment returns ErrFragmentUndefined", func(t *testing.T) { run(t, ` { dog { ...undefinedFragment } }`, - Fragments(), Invalid, withExpectNormalizationError()) + Fragments(), Invalid, withExpectNormalizationError(), withValidationErrors("undefinedFragment undefined")) + }) + t.Run("Undefined fragment after valid fragment returns ErrFragmentUndefined", func(t *testing.T) { + run(t, ` + { + cat { + ...validCatFragment + } + dog { + ...undefinedFragment + } + } + fragment validCatFragment on Cat { + name + meowVolume + }`, + Fragments(), Invalid, withExpectNormalizationError(), withValidationErrors("undefinedFragment undefined")) }) }) t.Run("5.5.2.2 Fragment spreads must not form cycles", func(t *testing.T) { @@ -2566,7 +2582,7 @@ func TestExecutionValidation(t *testing.T) { barkVolume ...nameFragment }`, - Fragments(), Invalid) + Fragments(), Invalid, withValidationErrors("external: fragment spread: barkVolumeFragment forms fragment cycle")) }) t.Run("136", func(t *testing.T) { run(t, ` @@ -2669,6 +2685,18 @@ func TestExecutionValidation(t *testing.T) { }`, Fragments(), Valid) }) + t.Run("Spreading a fragment on an invalid type returns ErrInvalidFragmentSpread", func(t *testing.T) { + run(t, ` + { + dog { + ...invalidCatFragment + } + } + fragment invalidCatFragment on Cat { + meowVolume + }`, + Fragments(), Invalid, withValidationErrors("external: fragment spread: fragment invalidCatFragment must be spread on type Cat and not type Dog")) + }) }) t.Run("5.5.2.3.2 Abstract Spreads in Object Scope", func(t *testing.T) { t.Run("139", func(t *testing.T) { diff --git a/pkg/engine/datasource/graphql_datasource/graphql_datasource.go b/pkg/engine/datasource/graphql_datasource/graphql_datasource.go index 26052b2351..7890f75aa6 100644 --- a/pkg/engine/datasource/graphql_datasource/graphql_datasource.go +++ b/pkg/engine/datasource/graphql_datasource/graphql_datasource.go @@ -395,14 +395,8 @@ func (p *Planner) EnterSelectionSet(ref int) { p.upstreamOperation.InlineFragments[parent.Ref].SelectionSet = set.Ref } p.nodes = append(p.nodes, set) - // Abstract meaning interface or union if p.visitor.Walker.EnclosingTypeDefinition.Kind.IsAbstractType() { - // Always include __typename in abstract type selection sets. This is - // done because child fields may be federated and __typename will be - // needed for representations. While it would be possible to determine - // exactly when __typename is needed, there's no harm in just always - // including it. - p.addTypenameToSelectionSet(set.Ref) + // Adding the typename to abstract (unions and interfaces) types is handled elsewhere return } @@ -522,7 +516,7 @@ func (p *Planner) EnterField(ref int) { } fieldConfiguration := p.visitor.Config.Fields.ForTypeField(enclosingTypeName, fieldName) - if fieldConfiguration == nil && fieldName != "__typename" { + if fieldConfiguration == nil { p.addField(ref) return } @@ -535,10 +529,6 @@ func (p *Planner) EnterField(ref int) { upstreamFieldRef := p.nodes[len(p.nodes)-1].Ref - if fieldConfiguration == nil { - return - } - for i := range fieldConfiguration.Arguments { argumentConfiguration := fieldConfiguration.Arguments[i] p.configureArgument(upstreamFieldRef, ref, *fieldConfiguration, argumentConfiguration) diff --git a/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go b/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go index dd5372040f..3f266ef9e4 100644 --- a/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go +++ b/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go @@ -118,7 +118,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$1$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($id: ID!){droid(id: $id){name aliased: name friends {__typename name} primaryFunction} hero {__typename name} stringList nestedStringList}","variables":{"id":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$1$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($id: ID!){droid(id: $id){name aliased: name friends {name} primaryFunction} hero {name} stringList nestedStringList}","variables":{"id":$$0$$}}}`, Variables: resolve.NewVariables( &resolve.ContextVariable{ Path: []string{"id"}, @@ -295,7 +295,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {__typename id displayName}}"}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {id displayName}}"}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, }, @@ -370,7 +370,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($skip: Boolean!){user {__typename id displayName @skip(if: $skip)}}","variables":{"skip":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($skip: Boolean!){user {id displayName @skip(if: $skip)}}","variables":{"skip":$$0$$}}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, Variables: resolve.NewVariables( @@ -455,7 +455,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($skip: Boolean!){user {__typename id displayName __typename @skip(if: $skip)}}","variables":{"skip":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($skip: Boolean!){user {id displayName __typename @skip(if: $skip)}}","variables":{"skip":$$0$$}}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), Variables: resolve.NewVariables(&resolve.ContextVariable{ Path: []string{"skip"}, @@ -556,7 +556,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($skip: Boolean!){user {__typename ... @skip(if: $skip){__typename id displayName}}}","variables":{"skip":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($skip: Boolean!){user {... @skip(if: $skip){id displayName}}}","variables":{"skip":$$0$$}}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, Variables: resolve.NewVariables( @@ -580,6 +580,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"id"}, }, + OnTypeNames: [][]byte{[]byte("RegisteredUser")}, SkipDirectiveDefined: true, SkipVariableName: "skip", }, @@ -588,6 +589,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"displayName"}, }, + OnTypeNames: [][]byte{[]byte("RegisteredUser")}, SkipDirectiveDefined: true, SkipVariableName: "skip", }, @@ -643,7 +645,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($include: Boolean!){user {__typename ... @include(if: $include){__typename id displayName}}}","variables":{"include":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($include: Boolean!){user {... @include(if: $include){id displayName}}}","variables":{"include":$$0$$}}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, Variables: resolve.NewVariables( @@ -667,6 +669,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"id"}, }, + OnTypeNames: [][]byte{[]byte("RegisteredUser")}, IncludeDirectiveDefined: true, IncludeVariableName: "include", }, @@ -675,6 +678,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"displayName"}, }, + OnTypeNames: [][]byte{[]byte("RegisteredUser")}, IncludeDirectiveDefined: true, IncludeVariableName: "include", }, @@ -728,7 +732,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {__typename id}}"}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {id}}"}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, }, @@ -797,7 +801,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {__typename id displayName}}"}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {id displayName}}"}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, }, @@ -872,7 +876,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($include: Boolean!){user {__typename id displayName @include(if: $include)}}","variables":{"include":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($include: Boolean!){user {id displayName @include(if: $include)}}","variables":{"include":$$0$$}}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, Variables: resolve.NewVariables( @@ -955,7 +959,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {__typename id displayName}}"}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {id displayName}}"}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, }, @@ -1029,7 +1033,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {__typename id}}"}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {id}}"}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, }, @@ -1101,7 +1105,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {__typename id displayName ... on RegisteredUser {hasVerifiedEmail}}}"}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"{user {id displayName __typename ... on RegisteredUser {hasVerifiedEmail}}}"}}`, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, }, @@ -1131,7 +1135,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.Boolean{ Path: []string{"hasVerifiedEmail"}, }, - OnTypeName: []byte("RegisteredUser"), + OnTypeNames: [][]byte{[]byte("RegisteredUser")}, }, }, }, @@ -1278,7 +1282,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($heroId: ID!){droid(id: $heroId){name} hero {__typename id}}","variables":{"heroId":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($heroId: ID!){droid(id: $heroId){name} hero {id}}","variables":{"heroId":$$0$$}}}`, Variables: resolve.NewVariables( &resolve.ContextVariable{ Path: []string{"heroId"}, @@ -1402,7 +1406,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$2$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($id: ID!, $heroName: String!){droid(id: $id){name aliased: name friends {__typename name} primaryFunction} hero {__typename name} search(name: $heroName){__typename ... on Droid {primaryFunction}} stringList nestedStringList}","variables":{"heroName":$$1$$,"id":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$2$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($id: ID!, $heroName: String!){droid(id: $id){name aliased: name friends {name} primaryFunction} hero {name} search(name: $heroName){__typename ... on Droid {primaryFunction}} stringList nestedStringList}","variables":{"heroName":$$1$$,"id":$$0$$}}}`, Variables: resolve.NewVariables( &resolve.ContextVariable{ Path: []string{"id"}, @@ -1501,7 +1505,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"primaryFunction"}, }, - OnTypeName: []byte("Droid"), + OnTypeNames: [][]byte{[]byte("Droid")}, }, }, }, @@ -1646,7 +1650,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$4$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($id: ID!, $a: String! @onVariable, $input: SearchInput!, $options: JSON)@onOperation {api_droid: droid(id: $id){name @format aliased: name friends {__typename name} primaryFunction} api_hero: hero {__typename name ... on Human {height}} api_stringList: stringList renamed: nestedStringList api_search: search(name: $a){__typename ... on Droid {primaryFunction}} api_searchWithInput: searchWithInput(input: $input){__typename ... on Droid {primaryFunction}} withOptions: searchWithInput(input: {options: $options}){__typename ... on Droid {primaryFunction}}}","variables":{"options":$$3$$,"input":$$2$$,"a":$$1$$,"id":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$4$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($id: ID!, $a: String! @onVariable, $input: SearchInput!, $options: JSON)@onOperation {api_droid: droid(id: $id){name @format aliased: name friends {name} primaryFunction} api_hero: hero {name __typename ... on Human {height}} api_stringList: stringList renamed: nestedStringList api_search: search(name: $a){__typename ... on Droid {primaryFunction}} api_searchWithInput: searchWithInput(input: $input){__typename ... on Droid {primaryFunction}} withOptions: searchWithInput(input: {options: $options}){__typename ... on Droid {primaryFunction}}}","variables":{"options":$$3$$,"input":$$2$$,"a":$$1$$,"id":$$0$$}}}`, Variables: resolve.NewVariables( &resolve.ContextVariable{ Path: []string{"id"}, @@ -1738,7 +1742,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"height"}, }, - OnTypeName: []byte("Human"), + OnTypeNames: [][]byte{[]byte("Human")}, }, }, }, @@ -1780,7 +1784,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"primaryFunction"}, }, - OnTypeName: []byte("Droid"), + OnTypeNames: [][]byte{[]byte("Droid")}, }, }, }, @@ -1798,7 +1802,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"primaryFunction"}, }, - OnTypeName: []byte("Droid"), + OnTypeNames: [][]byte{[]byte("Droid")}, }, }, }, @@ -1816,7 +1820,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"primaryFunction"}, }, - OnTypeName: []byte("Droid"), + OnTypeNames: [][]byte{[]byte("Droid")}, }, }, }, @@ -2039,7 +2043,7 @@ func TestGraphQLDataSource(t *testing.T) { }, }, }, - OnTypeName: []byte("Product"), + OnTypeNames: [][]byte{[]byte("Product")}, }, }, }, @@ -2246,7 +2250,7 @@ func TestGraphQLDataSource(t *testing.T) { Data: &resolve.Object{ Fetch: &resolve.SingleFetch{ BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($birthdate: Date!){heroByBirthdate(birthdate: $birthdate){__typename name}}","variables":{"birthdate":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","body":{"query":"query($birthdate: Date!){heroByBirthdate(birthdate: $birthdate){name}}","variables":{"birthdate":$$0$$}}}`, DataSource: &Source{}, Variables: resolve.NewVariables( &resolve.ContextVariable{ @@ -3451,8 +3455,8 @@ func TestGraphQLDataSource(t *testing.T) { }, }, { - OnTypeName: []byte("NamespaceCreated"), - Name: []byte("namespace"), + OnTypeNames: [][]byte{[]byte("NamespaceCreated")}, + Name: []byte("namespace"), Value: &resolve.Object{ Path: []string{"namespace"}, Fields: []*resolve.Field{ @@ -3474,15 +3478,15 @@ func TestGraphQLDataSource(t *testing.T) { }, }, { - OnTypeName: []byte("Error"), - Name: []byte("code"), + OnTypeNames: [][]byte{[]byte("Error")}, + Name: []byte("code"), Value: &resolve.String{ Path: []string{"code"}, }, }, { - OnTypeName: []byte("Error"), - Name: []byte("message"), + OnTypeNames: [][]byte{[]byte("Error")}, + Name: []byte("message"), Value: &resolve.String{ Path: []string{"message"}, }, @@ -4161,7 +4165,7 @@ func TestGraphQLDataSource(t *testing.T) { Data: &resolve.Object{ Fetch: &resolve.SingleFetch{ BufferId: 0, - Input: `{"method":"POST","url":"http://user.service","body":{"query":"query($a: ID!){user(id: $a){id name {first last} username birthDate __typename ssn}}","variables":{"a":$$0$$}}}`, + Input: `{"method":"POST","url":"http://user.service","body":{"query":"query($a: ID!){user(id: $a){id name {first last} username birthDate __typename account {__typename ... on PasswordAccount {email} ... on SMSAccount {number}} metadata {name address description} ssn}}","variables":{"a":$$0$$}}}`, DataSource: &Source{}, Variables: resolve.NewVariables( &resolve.ObjectVariable{ @@ -4181,7 +4185,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.BatchFetch{ Fetch: &resolve.SingleFetch{ BufferId: 1, - Input: `{"method":"POST","url":"http://product.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {vehicle {__typename id description price}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, + Input: `{"method":"POST","url":"http://product.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {vehicle {id description price}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, Variables: resolve.NewVariables( &resolve.ObjectVariable{ Path: []string{"id"}, @@ -4294,7 +4298,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"email"}, }, - OnTypeName: []byte("PasswordAccount"), + OnTypeNames: [][]byte{[]byte("PasswordAccount")}, }, { Name: []byte("number"), @@ -4302,7 +4306,7 @@ func TestGraphQLDataSource(t *testing.T) { Nullable: true, Path: []string{"number"}, }, - OnTypeName: []byte("SMSAccount"), + OnTypeNames: [][]byte{[]byte("SMSAccount")}, }, }, }, @@ -4366,7 +4370,7 @@ func TestGraphQLDataSource(t *testing.T) { ChildNodes: []plan.TypeField{ { TypeName: "User", - FieldNames: []string{"id", "name", "username", "birthDate", "metaData", "ssn"}, + FieldNames: []string{"id", "name", "username", "birthDate", "metadata", "ssn"}, }, { TypeName: "UserMetadata", @@ -4479,7 +4483,7 @@ func TestGraphQLDataSource(t *testing.T) { Data: &resolve.Object{ Fetch: &resolve.SingleFetch{ BufferId: 0, - Input: `{"method":"POST","url":"http://user.service","body":{"query":"query($a: ID!){user(id: $a){id name {first last} username birthDate __typename ssn}}","variables":{"a":$$0$$}}}`, + Input: `{"method":"POST","url":"http://user.service","body":{"query":"query($a: ID!){user(id: $a){id name {first last} username birthDate account {__typename ... on PasswordAccount {email} ... on SMSAccount {number}} metadata {name address description} __typename ssn}}","variables":{"a":$$0$$}}}`, DataSource: &Source{}, Variables: resolve.NewVariables( &resolve.ObjectVariable{ @@ -4499,7 +4503,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.BatchFetch{ Fetch: &resolve.SingleFetch{ BufferId: 1, - Input: `{"method":"POST","url":"http://product.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {vehicle {__typename id description price}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, + Input: `{"method":"POST","url":"http://product.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {vehicle {id description price}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, Variables: resolve.NewVariables( &resolve.ObjectVariable{ Path: []string{"id"}, @@ -4573,7 +4577,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"email"}, }, - OnTypeName: []byte("PasswordAccount"), + OnTypeNames: [][]byte{[]byte("PasswordAccount")}, }, { Name: []byte("number"), @@ -4581,7 +4585,7 @@ func TestGraphQLDataSource(t *testing.T) { Nullable: true, Path: []string{"number"}, }, - OnTypeName: []byte("SMSAccount"), + OnTypeNames: [][]byte{[]byte("SMSAccount")}, }, }, }, @@ -4684,7 +4688,7 @@ func TestGraphQLDataSource(t *testing.T) { ChildNodes: []plan.TypeField{ { TypeName: "User", - FieldNames: []string{"id", "name", "username", "birthDate", "metaData", "ssn"}, + FieldNames: []string{"id", "name", "username", "birthDate", "metadata", "ssn"}, }, { TypeName: "UserMetadata", @@ -5853,7 +5857,7 @@ func TestGraphQLDataSource(t *testing.T) { Data: &resolve.Object{ Fetch: &resolve.SingleFetch{ BufferId: 0, - Input: `{"method":"POST","url":"http://user.service","body":{"query":"{self {__typename id ... on User {uid: id username id}}}"}}`, + Input: `{"method":"POST","url":"http://user.service","body":{"query":"{self {id __typename ... on User {uid: id username}}}"}}`, DataSource: &Source{}, DataSourceIdentifier: []byte("graphql_datasource.Source"), ProcessResponseConfig: resolve.ProcessResponseConfig{ExtractGraphqlResponse: true}, @@ -5891,13 +5895,12 @@ func TestGraphQLDataSource(t *testing.T) { Path: []string{"self"}, Nullable: true, Fields: []*resolve.Field{ - // there is a bug with interfaces that consumes this - //{ - // Name: []byte("id"), - // Value: &resolve.String{ - // Path: []string{"id"}, - // }, - //}, + { + Name: []byte("id"), + Value: &resolve.String{ + Path: []string{"id"}, + }, + }, { Name: []byte("__typename"), TypeName: "Identity", @@ -5912,14 +5915,14 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"uid"}, }, - OnTypeName: []byte("User"), + OnTypeNames: [][]byte{[]byte("User")}, }, { Name: []byte("username"), Value: &resolve.String{ Path: []string{"username"}, }, - OnTypeName: []byte("User"), + OnTypeNames: [][]byte{[]byte("User")}, }, { HasBuffer: true, @@ -5940,7 +5943,7 @@ func TestGraphQLDataSource(t *testing.T) { }, }, }, - OnTypeName: []byte("User"), + OnTypeNames: [][]byte{[]byte("User")}, }, }, }, @@ -6113,14 +6116,14 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"extension"}, }, - OnTypeName: []byte("Image"), + OnTypeNames: [][]byte{[]byte("Image")}, }, { Name: []byte("length"), Value: &resolve.String{ Path: []string{"length"}, }, - OnTypeName: []byte("Video"), + OnTypeNames: [][]byte{[]byte("Video")}, }, }, }, @@ -6128,7 +6131,7 @@ func TestGraphQLDataSource(t *testing.T) { }, }, }, - OnTypeName: []byte("User"), + OnTypeNames: [][]byte{[]byte("User")}, }, }, }, @@ -6285,11 +6288,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.BatchFetch{ Fetch: &resolve.SingleFetch{ BufferId: 1, - // Note: __typename is included in the Cat and Dog inline fragments - // because the field were originally themselves in inline fragments - // that were inlined. The additional __typename selections are - // harmless. - Input: `{"method":"POST","url":"http://pet.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {pets {__typename name ... on Cat {catField details {age}} ... on Dog {dogField species} details {hasOwner}}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, + Input: `{"method":"POST","url":"http://pet.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {pets {name __typename ... on Cat {catField details {age}} ... on Dog {dogField species} details {hasOwner}}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, Variables: resolve.NewVariables( &resolve.ObjectVariable{ Path: []string{"id"}, @@ -6336,7 +6335,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"catField"}, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("details"), @@ -6351,21 +6350,21 @@ func TestGraphQLDataSource(t *testing.T) { }, }, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("dogField"), Value: &resolve.String{ Path: []string{"dogField"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, { Name: []byte("species"), Value: &resolve.String{ Path: []string{"species"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, { Name: []byte("details"), @@ -6493,28 +6492,28 @@ func TestGraphQLDataSource(t *testing.T) { t.Run("featuring consecutive inline fragments (shared selection in middle)", RunTest( federatedSchemaWithComplexNestedFragments, ` - query TestQuery { - user { - username - pets { - ... on Cat { - catField + query TestQuery { + user { + username + pets { + ... on Cat { + catField + details { + age + } + } + name + ... on Dog { + dogField + species + } details { - age + hasOwner } } - name - ... on Dog { - dogField - species - } - details { - hasOwner - } } } - } - `, + `, "TestQuery", &plan.SynchronousResponsePlan{ Response: &resolve.GraphQLResponse{ @@ -6535,11 +6534,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.BatchFetch{ Fetch: &resolve.SingleFetch{ BufferId: 1, - // Note: __typename is included in the Cat and Dog inline fragments - // because the field were originally themselves in inline fragments - // that were inlined. The additional __typename selections are - // harmless. - Input: `{"method":"POST","url":"http://pet.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {pets {__typename ... on Cat {catField details {age}} name ... on Dog {dogField species} details {hasOwner}}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, + Input: `{"method":"POST","url":"http://pet.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {pets {__typename ... on Cat {catField details {age}} name ... on Dog {dogField species} details {hasOwner}}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, Variables: resolve.NewVariables( &resolve.ObjectVariable{ Path: []string{"id"}, @@ -6580,7 +6575,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"catField"}, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("details"), @@ -6595,7 +6590,7 @@ func TestGraphQLDataSource(t *testing.T) { }, }, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("name"), @@ -6608,14 +6603,14 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"dogField"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, { Name: []byte("species"), Value: &resolve.String{ Path: []string{"species"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, { Name: []byte("details"), @@ -6743,28 +6738,28 @@ func TestGraphQLDataSource(t *testing.T) { t.Run("featuring consecutive inline fragments (shared selection at bottom)", RunTest( federatedSchemaWithComplexNestedFragments, ` - query TestQuery { - user { - username - pets { - ... on Cat { - catField + query TestQuery { + user { + username + pets { + ... on Cat { + catField + details { + age + } + } + ... on Dog { + dogField + species + } details { - age + hasOwner } + name } - ... on Dog { - dogField - species - } - details { - hasOwner - } - name } } - } - `, + `, "TestQuery", &plan.SynchronousResponsePlan{ Response: &resolve.GraphQLResponse{ @@ -6830,7 +6825,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"catField"}, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("details"), @@ -6845,21 +6840,21 @@ func TestGraphQLDataSource(t *testing.T) { }, }, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("dogField"), Value: &resolve.String{ Path: []string{"dogField"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, { Name: []byte("species"), Value: &resolve.String{ Path: []string{"species"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, { Name: []byte("details"), @@ -6993,41 +6988,41 @@ func TestGraphQLDataSource(t *testing.T) { t.Run("Federation with field query (defined in pet subgraph) featuring consecutive inline union fragments", RunTest( ` - type Query { - user: User - } - type User { + type Query { + user: User + } + type User { id: ID! - username: String! - pets: [CatOrDog!]! - } - type Cat { - name: String! - catField: String! - } - type Dog { - name: String! - dogField: String! - } - union CatOrDog = Cat | Dog - `, + username: String! + pets: [CatOrDog!]! + } + type Cat { + name: String! + catField: String! + } + type Dog { + name: String! + dogField: String! + } + union CatOrDog = Cat | Dog + `, ` - query TestQuery { - user { - username - pets { - ... on Cat { + query TestQuery { + user { + username + pets { + ... on Cat { name - catField - } - ... on Dog { + catField + } + ... on Dog { name - dogField - } - } - } - } - `, + dogField + } + } + } + } + `, "TestQuery", &plan.SynchronousResponsePlan{ Response: &resolve.GraphQLResponse{ @@ -7048,11 +7043,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.BatchFetch{ Fetch: &resolve.SingleFetch{ BufferId: 1, - // Note: __typename is included in the Cat and Dog inline fragments - // because the field were originally themselves in inline fragments - // that were inlined. The additional __typename selections are - // harmless. - Input: `{"method":"POST","url":"http://pet.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {pets {__typename ... on Cat {name catField} ... on Dog {name dogField}}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, + Input: `{"method":"POST","url":"http://pet.service","body":{"query":"query($representations: [_Any!]!){_entities(representations: $representations){__typename ... on User {pets {__typename ... on Cat {name catField} ... on Dog {name dogField}}}}}","variables":{"representations":[{"id":$$0$$,"__typename":"User"}]}}}`, Variables: resolve.NewVariables( &resolve.ObjectVariable{ Path: []string{"id"}, @@ -7093,28 +7084,28 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"name"}, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("catField"), Value: &resolve.String{ Path: []string{"catField"}, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { Name: []byte("name"), Value: &resolve.String{ Path: []string{"name"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, { Name: []byte("dogField"), Value: &resolve.String{ Path: []string{"dogField"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, }, }, @@ -7177,20 +7168,20 @@ func TestGraphQLDataSource(t *testing.T) { Federation: FederationConfiguration{ Enabled: true, ServiceSDL: ` - union CatOrDog = Cat | Dog - type Cat { - name: String! - catField: String! - } - type Dog { - name: String! - dogField: String! - } + union CatOrDog = Cat | Dog + type Cat { + name: String! + catField: String! + } + type Dog { + name: String! + dogField: String! + } extend type User @key(fields: "id") { id: ID! @external pets: [CatOrDog!]! } - `, + `, }, }), Factory: federationFactory, @@ -7310,7 +7301,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"name"}, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { HasBuffer: true, @@ -7319,7 +7310,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"catField"}, }, - OnTypeName: []byte("Cat"), + OnTypeNames: [][]byte{[]byte("Cat")}, }, { HasBuffer: true, @@ -7328,7 +7319,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"name"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, { HasBuffer: true, @@ -7337,7 +7328,7 @@ func TestGraphQLDataSource(t *testing.T) { Value: &resolve.String{ Path: []string{"dogField"}, }, - OnTypeName: []byte("Dog"), + OnTypeNames: [][]byte{[]byte("Dog")}, }, }, }, @@ -7440,11 +7431,21 @@ func TestGraphQLDataSource(t *testing.T) { FieldName: "name", RequiresFields: []string{"id"}, }, + { + TypeName: "Cat", + FieldName: "catField", + RequiresFields: []string{"id"}, + }, { TypeName: "Dog", FieldName: "name", RequiresFields: []string{"id"}, }, + { + TypeName: "Dog", + FieldName: "dogField", + RequiresFields: []string{"id"}, + }, }, DisableResolveFieldPositions: true, })) @@ -7469,7 +7470,7 @@ func TestGraphQLDataSource(t *testing.T) { Fetch: &resolve.SingleFetch{ DataSource: &Source{}, BufferId: 0, - Input: `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$2$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($droidId: ID!, $reviewId: ReviewID!){droid(id: $droidId){name aliased: name friends {__typename name} primaryFunction} review(id: $reviewId){stars}}","variables":{"reviewId":$$1$$,"droidId":$$0$$}}}`, + Input: `{"method":"POST","url":"https://swapi.com/graphql","header":{"Authorization":["$$2$$"],"Invalid-Template":["{{ request.headers.Authorization }}"]},"body":{"query":"query($droidId: ID!, $reviewId: ReviewID!){droid(id: $droidId){name aliased: name friends {name} primaryFunction} review(id: $reviewId){stars}}","variables":{"reviewId":$$1$$,"droidId":$$0$$}}}`, Variables: resolve.NewVariables( &resolve.ContextVariable{ Path: []string{"droidId"}, @@ -8587,7 +8588,7 @@ type Droid implements Character { friends: [Character] } -type Startship { +type Starship { name: String! length: Float! }` @@ -8663,7 +8664,7 @@ type Droid implements Character { friends: [Character] } -type Startship { +type Starship { name: String! length: Float! }` @@ -8743,7 +8744,7 @@ type Droid implements Character { friends: [Character] } -type Startship { +type Starship { name: String! length: Float! }` @@ -8820,7 +8821,7 @@ type Droid_api implements Character_api { friends: [Character_api] } -type Startship_api { +type Starship_api { name: String! length: Float! }` diff --git a/pkg/engine/plan/plan.go b/pkg/engine/plan/plan.go index fe2b0d8388..2f49ecb162 100644 --- a/pkg/engine/plan/plan.go +++ b/pkg/engine/plan/plan.go @@ -156,7 +156,7 @@ type ArgumentConfiguration struct { type DataSourceConfiguration struct { // RootNodes - defines the nodes where the responsibility of the DataSource begins - // When you enter a node and it is not a child node + // When you enter a node, and it is not a child node // when you have entered into a field which representing data source - it means that we starting a new planning stage RootNodes []TypeField // ChildNodes - describes additional fields which will be requested along with fields which has a datasources @@ -224,6 +224,7 @@ func NewPlanner(ctx context.Context, config Configuration) *Planner { requiredFieldsWalker.RegisterEnterDocumentVisitor(requiredFieldsV) requiredFieldsWalker.RegisterEnterOperationVisitor(requiredFieldsV) requiredFieldsWalker.RegisterEnterFieldVisitor(requiredFieldsV) + requiredFieldsWalker.RegisterLeaveDocumentVisitor(requiredFieldsV) // configuration @@ -308,7 +309,8 @@ func (p *Planner) Plan(operation, definition *ast.Document, operationName string isNested := p.planningVisitor.planners[key].isNestedPlanner() err := p.planningVisitor.planners[key].planner.Register(p.planningVisitor, config, isNested) if err != nil { - p.planningWalker.StopWithInternalErr(err) + report.AddInternalError(err) + return } } @@ -510,7 +512,6 @@ func (v *Visitor) LeaveSelectionSet(_ int) { } func (v *Visitor) EnterField(ref int) { - if v.skipField(ref) { return } @@ -528,7 +529,7 @@ func (v *Visitor) EnterField(ref int) { Path: []string{"__typename"}, IsTypeName: true, }, - OnTypeName: v.resolveOnTypeName(), + OnTypeNames: v.resolveOnTypeNames(), TypeName: v.Walker.EnclosingTypeDefinition.NameString(v.Definition), Position: v.resolveFieldPosition(ref), SkipDirectiveDefined: skip, @@ -575,7 +576,7 @@ func (v *Visitor) EnterField(ref int) { Value: v.resolveFieldValue(ref, fieldDefinitionType, true, path), HasBuffer: hasBuffer, BufferID: bufferID, - OnTypeName: v.resolveOnTypeName(), + OnTypeNames: v.resolveOnTypeNames(), Position: v.resolveFieldPosition(ref), SkipDirectiveDefined: skip, SkipVariableName: skipVariableName, @@ -648,7 +649,7 @@ func (v *Visitor) resolveInclude(directiveRefs []int) (bool, string) { return false, "" } -func (v *Visitor) resolveOnTypeName() []byte { +func (v *Visitor) resolveOnTypeNames() [][]byte { if len(v.Walker.Ancestors) < 2 { return nil } @@ -657,7 +658,29 @@ func (v *Visitor) resolveOnTypeName() []byte { return nil } typeName := v.Operation.InlineFragmentTypeConditionName(inlineFragment.Ref) - return v.Config.Types.RenameTypeNameOnMatchBytes(typeName) + if typeName == nil { + typeName = v.Walker.EnclosingTypeDefinition.NameBytes(v.Definition) + } + node, exists := v.Definition.NodeByName(typeName) + // If not an interface, return the concrete type + if !exists || !node.Kind.IsAbstractType() { + return [][]byte{v.Config.Types.RenameTypeNameOnMatchBytes(typeName)} + } + if node.Kind == ast.NodeKindUnionTypeDefinition { + // This should never be true. If it is, it's an error + panic("resolveOnTypeNames called with a union type") + } + // We're dealing with an interface, so add all objects that implement this interface to the slice + onTypeNames := make([][]byte, 0, 2) + for objectTypeDefinitionRef := range v.Definition.ObjectTypeDefinitions { + if v.Definition.ObjectTypeDefinitionImplementsInterface(objectTypeDefinitionRef, typeName) { + onTypeNames = append(onTypeNames, v.Definition.ObjectTypeDefinitionNameBytes(objectTypeDefinitionRef)) + } + } + if len(onTypeNames) < 1 { + return nil + } + return onTypeNames } func (v *Visitor) LeaveField(ref int) { @@ -1534,81 +1557,132 @@ func (c *configurationVisitor) EnterField(ref int) { fieldName := c.operation.FieldNameUnsafeString(ref) fieldAliasOrName := c.operation.FieldAliasOrNameString(ref) typeName := c.walker.EnclosingTypeDefinition.NameString(c.definition) - parent := c.walker.Path.DotDelimitedString() - current := parent + "." + fieldAliasOrName + parentPath := c.walker.Path.DotDelimitedString() + currentPath := parentPath + "." + fieldAliasOrName root := c.walker.Ancestors[0] if root.Kind != ast.NodeKindOperationDefinition { return } - isSubscription := c.isSubscription(root.Ref, current) + isSubscription := c.isSubscription(root.Ref, currentPath) for i, plannerConfig := range c.planners { planningBehaviour := plannerConfig.planner.DataSourcePlanningBehavior() - if plannerConfig.hasParent(parent) && plannerConfig.hasRootNode(typeName, fieldName) && planningBehaviour.MergeAliasedRootNodes { - // same parent + root node = root sibling - c.planners[i].paths = append(c.planners[i].paths, pathConfiguration{path: current, shouldWalkFields: true}) - c.fieldBuffers[ref] = plannerConfig.bufferID + if fieldAliasOrName == "__typename" && planningBehaviour.IncludeTypeNameFields { + c.planners[i].paths = append(c.planners[i].paths, pathConfiguration{path: currentPath, shouldWalkFields: true}) return } - if plannerConfig.hasPath(parent) && plannerConfig.hasChildNode(typeName, fieldName) { - // has parent path + has child node = child - c.planners[i].paths = append(c.planners[i].paths, pathConfiguration{path: current, shouldWalkFields: true}) + if plannerConfig.hasParent(parentPath) && plannerConfig.hasRootNode(typeName, fieldName) && planningBehaviour.MergeAliasedRootNodes { + // same parent + root node = root sibling + c.planners[i].paths = append(c.planners[i].paths, pathConfiguration{path: currentPath, shouldWalkFields: true}) + c.fieldBuffers[ref] = plannerConfig.bufferID return } - if fieldAliasOrName == "__typename" && planningBehaviour.IncludeTypeNameFields { - c.planners[i].paths = append(c.planners[i].paths, pathConfiguration{path: current, shouldWalkFields: true}) - return + if plannerConfig.hasPath(parentPath) { + if plannerConfig.hasChildNode(typeName, fieldName) { + // has parent path + has child node = child + c.planners[i].paths = append(c.planners[i].paths, pathConfiguration{path: currentPath, shouldWalkFields: true}) + return + } + + if pathAdded := c.addPlannerPathForUnionChildOfObjectParent(ref, i, currentPath); pathAdded { + return + } + + if pathAdded := c.addPlannerPathForChildOfAbstractParent(i, typeName, fieldName, currentPath); pathAdded { + return + } } } for i, config := range c.config.DataSources { - if config.HasRootNode(typeName, fieldName) { - var ( - bufferID int - ) - if !isSubscription { - bufferID = c.nextBufferID() - c.fieldBuffers[ref] = bufferID - } - planner := c.config.DataSources[i].Factory.Planner(c.ctx) - isParentAbstract := c.isParentTypeNodeAbstractType() - paths := []pathConfiguration{ + if !config.HasRootNode(typeName, fieldName) { + continue + } + var ( + bufferID int + ) + if !isSubscription { + bufferID = c.nextBufferID() + c.fieldBuffers[ref] = bufferID + } + planner := c.config.DataSources[i].Factory.Planner(c.ctx) + isParentAbstract := c.isParentTypeNodeAbstractType() + paths := []pathConfiguration{ + { + path: currentPath, + shouldWalkFields: true, + }, + } + if isParentAbstract { + // if the parent is abstract, we add the parent path as well + // this will ensure that we're walking into and out of the root inline fragments + // otherwise, we'd only walk into the fields inside the inline fragments in the root, + // so we'd miss the selection sets and inline fragments in the root + paths = append([]pathConfiguration{ { - path: current, - shouldWalkFields: true, + path: parentPath, + shouldWalkFields: false, }, - } - if isParentAbstract { - // if the parent is abstract, we add the parent path as well - // this will ensure that we're walking into and out of the root inline fragments - // otherwise, we'd only walk into the fields inside the inline fragments in the root, - // so we'd miss the selection sets and inline fragments in the root - paths = append([]pathConfiguration{ - { - path: parent, - shouldWalkFields: false, - }, - }, paths...) - } - c.planners = append(c.planners, plannerConfiguration{ - bufferID: bufferID, - parentPath: parent, - planner: planner, - paths: paths, - dataSourceConfiguration: config, - }) - fieldDefinition, ok := c.walker.FieldDefinition(ref) - if !ok { - continue - } - c.fetches = append(c.fetches, objectFetchConfiguration{ - bufferID: bufferID, - planner: planner, - isSubscription: isSubscription, - fieldRef: ref, - fieldDefinitionRef: fieldDefinition, - }) - return + }, paths...) } + c.planners = append(c.planners, plannerConfiguration{ + bufferID: bufferID, + parentPath: parentPath, + planner: planner, + paths: paths, + dataSourceConfiguration: config, + }) + fieldDefinition, ok := c.walker.FieldDefinition(ref) + if !ok { + continue + } + c.fetches = append(c.fetches, objectFetchConfiguration{ + bufferID: bufferID, + planner: planner, + isSubscription: isSubscription, + fieldRef: ref, + fieldDefinitionRef: fieldDefinition, + }) + return + } +} + +func (c *configurationVisitor) addPlannerPathForUnionChildOfObjectParent(fieldRef int, plannerIndex int, currentPath string) (pathAdded bool) { + if c.walker.EnclosingTypeDefinition.Kind != ast.NodeKindObjectTypeDefinition { + return false + } + fieldDefRef, exists := c.definition.NodeFieldDefinitionByName(c.walker.EnclosingTypeDefinition, c.operation.FieldNameBytes(fieldRef)) + if !exists { + return false + } + + fieldDefTypeRef := c.definition.FieldDefinitionType(fieldDefRef) + fieldDefTypeName := c.definition.TypeNameBytes(fieldDefTypeRef) + node, ok := c.definition.NodeByName(fieldDefTypeName) + if !ok { + return false + } + + if node.Kind == ast.NodeKindUnionTypeDefinition { + c.planners[plannerIndex].paths = append(c.planners[plannerIndex].paths, pathConfiguration{path: currentPath, shouldWalkFields: true}) + return true } + return false +} + +func (c *configurationVisitor) addPlannerPathForChildOfAbstractParent( + plannerIndex int, typeName, fieldName, currentPath string, +) (pathAdded bool) { + if !c.isParentTypeNodeAbstractType() { + return false + } + // If the field is a root node in any of the data sources, the path shouldn't be handled here + for _, d := range c.config.DataSources { + if d.HasRootNode(typeName, fieldName) { + return false + } + } + // The path for this field should only be added if the parent path also exists on this planner + c.planners[plannerIndex].paths = append(c.planners[plannerIndex].paths, pathConfiguration{path: currentPath, shouldWalkFields: true}) + return true } func (c *configurationVisitor) isParentTypeNodeAbstractType() bool { @@ -1662,23 +1736,44 @@ func (c *configurationVisitor) isSubscription(root int, path string) bool { return strings.Count(path, ".") == 1 } +type skipFieldData struct { + selectionSetRef int + fieldConfig *FieldConfiguration + requiredField string + fieldPath string +} + type requiredFieldsVisitor struct { operation, definition *ast.Document walker *astvisitor.Walker config *Configuration operationName string skipFieldPaths []string + // selectedFieldPaths is a set of all explicitly selected field paths + selectedFieldPaths map[string]struct{} + // skipFieldDataPaths is to prevent appending duplicate skipFieldData to potentialSkipFieldDatas + skipFieldDataPaths map[string]struct{} + // potentialSkipFieldDatas is used in LeaveDocument to determine whether a required field should be skipped + // Must be a slice to preserve field order, which is why duplicates are handled with a set + potentialSkipFieldDatas []*skipFieldData } func (r *requiredFieldsVisitor) EnterDocument(_, _ *ast.Document) { r.skipFieldPaths = r.skipFieldPaths[:0] + r.selectedFieldPaths = make(map[string]struct{}) + r.potentialSkipFieldDatas = make([]*skipFieldData, 0) } func (r *requiredFieldsVisitor) EnterField(ref int) { typeName := r.walker.EnclosingTypeDefinition.NameString(r.definition) fieldName := r.operation.FieldNameUnsafeString(ref) fieldConfig := r.config.Fields.ForTypeField(typeName, fieldName) + path := r.walker.Path.DotDelimitedString() if fieldConfig == nil { + // Record all explicitly selected fields + // A field selected on an interface will have the same field path as a fragment on an object + // LeaveDocument uses this record to ensure only required fields that were not explicitly selected are skipped + r.selectedFieldPaths[fmt.Sprintf("%s.%s", path, fieldName)] = struct{}{} return } if len(fieldConfig.RequiresFields) == 0 { @@ -1688,12 +1783,24 @@ func (r *requiredFieldsVisitor) EnterField(ref int) { if selectionSet.Kind != ast.NodeKindSelectionSet { return } - for i := range fieldConfig.RequiresFields { - r.handleRequiredField(selectionSet.Ref, fieldConfig.RequiresFields[i]) + for _, requiredField := range fieldConfig.RequiresFields { + requiredFieldPath := fmt.Sprintf("%s.%s", path, requiredField) + // Prevent adding duplicates to the slice (order is necessary; hence, a separate map) + if _, ok := r.skipFieldDataPaths[requiredFieldPath]; ok { + continue + } + // For each required field, collect the data required to handle (in LeaveDocument) whether we should skip it + data := &skipFieldData{ + selectionSetRef: selectionSet.Ref, + fieldConfig: fieldConfig, + requiredField: requiredField, + fieldPath: requiredFieldPath, + } + r.potentialSkipFieldDatas = append(r.potentialSkipFieldDatas, data) } } -func (r *requiredFieldsVisitor) handleRequiredField(selectionSet int, requiredFieldName string) { +func (r *requiredFieldsVisitor) handleRequiredField(selectionSet int, requiredFieldName, fullFieldPath string) { for _, ref := range r.operation.SelectionSets[selectionSet].SelectionRefs { selection := r.operation.Selections[ref] if selection.Kind != ast.SelectionKindField { @@ -1705,10 +1812,10 @@ func (r *requiredFieldsVisitor) handleRequiredField(selectionSet int, requiredFi return } } - r.addRequiredField(requiredFieldName, selectionSet) + r.addRequiredField(requiredFieldName, selectionSet, fullFieldPath) } -func (r *requiredFieldsVisitor) addRequiredField(fieldName string, selectionSet int) { +func (r *requiredFieldsVisitor) addRequiredField(fieldName string, selectionSet int, fullFieldPath string) { field := ast.Field{ Name: r.operation.Input.AppendInputString(fieldName), } @@ -1718,8 +1825,7 @@ func (r *requiredFieldsVisitor) addRequiredField(fieldName string, selectionSet Ref: addedField.Ref, } r.operation.AddSelection(selectionSet, selection) - addedFieldPath := r.walker.Path.DotDelimitedString() + "." + fieldName - r.skipFieldPaths = append(r.skipFieldPaths, addedFieldPath) + r.skipFieldPaths = append(r.skipFieldPaths, fullFieldPath) } func (r *requiredFieldsVisitor) EnterOperationDefinition(ref int) { @@ -1729,3 +1835,13 @@ func (r *requiredFieldsVisitor) EnterOperationDefinition(ref int) { return } } + +func (r *requiredFieldsVisitor) LeaveDocument(_, _ *ast.Document) { + for _, data := range r.potentialSkipFieldDatas { + path := data.fieldPath + // If a field was not explicitly selected, skip it + if _, ok := r.selectedFieldPaths[path]; !ok { + r.handleRequiredField(data.selectionSetRef, data.requiredField, path) + } + } +} diff --git a/pkg/engine/plan/plan_test.go b/pkg/engine/plan/plan_test.go index e8f6676bbd..e88f0e6870 100644 --- a/pkg/engine/plan/plan_test.go +++ b/pkg/engine/plan/plan_test.go @@ -208,6 +208,58 @@ func TestPlanner_Plan(t *testing.T) { DefaultFlushIntervalMillis: 0, })) + t.Run("Union response type with interface fragments", test(testDefinition, ` + query SearchResults { + searchResults { + ... on Character { + name + } + ... on Vehicle { + length + } + } + } + `, "SearchResults", &SynchronousResponsePlan{ + Response: &resolve.GraphQLResponse{ + Data: &resolve.Object{ + Nullable: false, + Fields: []*resolve.Field{ + { + Name: []byte("searchResults"), + Value: &resolve.Array{ + Path: []string{"searchResults"}, + Nullable: true, + ResolveAsynchronous: false, + Item: &resolve.Object{ + Nullable: true, + Fields: []*resolve.Field{ + { + Name: []byte("name"), + Value: &resolve.String{ + Path: []string{"name"}, + Nullable: false, + }, + OnTypeNames: [][]byte{[]byte("Human"), []byte("Droid")}, + }, + { + Name: []byte("length"), + Value: &resolve.Float{ + Path: []string{"length"}, + Nullable: false, + }, + OnTypeNames: [][]byte{[]byte("Starship")}, + }, + }, + }, + }, + }, + }, + }, + }, + }, Configuration{ + DisableResolveFieldPositions: true, + })) + t.Run("operation selection", func(t *testing.T) { t.Run("should successfully plan a single named query by providing an operation name", test(testDefinition, ` query MyHero { @@ -524,6 +576,7 @@ type Query { hero: Character droid(id: ID!): Droid search(name: String!): SearchResult + searchResults: [SearchResult] } type Mutation { @@ -570,7 +623,12 @@ type Droid implements Character { favoriteEpisode: Episode } -type Startship { +interface Vehicle { + length: Float! +} + +type Starship implements Vehicle { name: String! length: Float! -}` +} +` diff --git a/pkg/engine/resolve/resolve.go b/pkg/engine/resolve/resolve.go index 7ce8563ef2..75fffd1645 100644 --- a/pkg/engine/resolve/resolve.go +++ b/pkg/engine/resolve/resolve.go @@ -1086,7 +1086,6 @@ func (r *Resolver) resolveObject(ctx *Context, object *Object, data []byte, obje first := true skipCount := 0 for i := range object.Fields { - if object.Fields[i].SkipDirectiveDefined { skip, err := jsonparser.GetBoolean(ctx.Variables, object.Fields[i].SkipVariableName) if err == nil && skip { @@ -1130,9 +1129,16 @@ func (r *Resolver) resolveObject(ctx *Context, object *Object, data []byte, obje } } - if object.Fields[i].OnTypeName != nil { + if object.Fields[i].OnTypeNames != nil { typeName, _, _, _ := jsonparser.Get(fieldData, "__typename") - if !bytes.Equal(typeName, object.Fields[i].OnTypeName) { + hasMatch := false + for _, onTypeName := range object.Fields[i].OnTypeNames { + if bytes.Equal(typeName, onTypeName) { + hasMatch = true + break + } + } + if !hasMatch { typeNameSkip = true // Restore the response elements that may have been reset above. ctx.responseElements = responseElements @@ -1374,7 +1380,7 @@ type Field struct { Stream *StreamField HasBuffer bool BufferID int - OnTypeName []byte + OnTypeNames [][]byte TypeName string SkipDirectiveDefined bool SkipVariableName string diff --git a/pkg/engine/resolve/resolve_test.go b/pkg/engine/resolve/resolve_test.go index 47f316fdf7..142fcff8bc 100644 --- a/pkg/engine/resolve/resolve_test.go +++ b/pkg/engine/resolve/resolve_test.go @@ -1207,10 +1207,10 @@ func TestResolver_ResolveNode(t *testing.T) { Item: &Object{ Fields: []*Field{ { - BufferID: 0, - HasBuffer: true, - OnTypeName: []byte("Dog"), - Name: []byte("name"), + BufferID: 0, + HasBuffer: true, + OnTypeNames: [][]byte{[]byte("Dog")}, + Name: []byte("name"), Value: &String{ Path: []string{"name"}, }, @@ -1273,8 +1273,8 @@ func TestResolver_ResolveNode(t *testing.T) { Nullable: false, Fields: []*Field{ { - OnTypeName: []byte("Cat"), - Name: []byte("name"), + OnTypeNames: [][]byte{[]byte("Cat")}, + Name: []byte("name"), Value: &String{ Path: []string{"name"}, }, @@ -1307,8 +1307,8 @@ func TestResolver_ResolveNode(t *testing.T) { Path: []string{"namespaceCreate"}, Fields: []*Field{ { - Name: []byte("namespace"), - OnTypeName: []byte("NamespaceCreated"), + Name: []byte("namespace"), + OnTypeNames: [][]byte{[]byte("NamespaceCreated")}, Value: &Object{ Path: []string{"namespace"}, Nullable: false, @@ -1331,16 +1331,16 @@ func TestResolver_ResolveNode(t *testing.T) { }, }, { - Name: []byte("code"), - OnTypeName: []byte("Error"), + Name: []byte("code"), + OnTypeNames: [][]byte{[]byte("Error")}, Value: &String{ Nullable: false, Path: []string{"code"}, }, }, { - Name: []byte("message"), - OnTypeName: []byte("Error"), + Name: []byte("message"), + OnTypeNames: [][]byte{[]byte("Error")}, Value: &String{ Nullable: false, Path: []string{"message"}, @@ -1372,10 +1372,10 @@ func TestResolver_ResolveNode(t *testing.T) { Item: &Object{ Fields: []*Field{ { - BufferID: 0, - HasBuffer: true, - OnTypeName: []byte("Dog"), - Name: []byte("name"), + BufferID: 0, + HasBuffer: true, + OnTypeNames: [][]byte{[]byte("Dog")}, + Name: []byte("name"), Value: &String{ Path: []string{"name"}, }, @@ -1420,10 +1420,10 @@ func TestResolver_ResolveNode(t *testing.T) { Nullable: true, Fields: []*Field{ { - BufferID: 0, - HasBuffer: false, - OnTypeName: []byte("Dog"), - Name: []byte("name"), + BufferID: 0, + HasBuffer: false, + OnTypeNames: [][]byte{[]byte("Dog")}, + Name: []byte("name"), Value: &String{ Path: []string{"name"}, }, @@ -1456,10 +1456,10 @@ func TestResolver_ResolveNode(t *testing.T) { Item: &Object{ Fields: []*Field{ { - BufferID: 0, - HasBuffer: true, - OnTypeName: []byte("Dog"), - Name: []byte("name"), + BufferID: 0, + HasBuffer: true, + OnTypeNames: [][]byte{[]byte("Dog")}, + Name: []byte("name"), Value: &String{ Path: []string{"name"}, }, diff --git a/pkg/graphql/execution_engine_v2_test.go b/pkg/graphql/execution_engine_v2_test.go index 823446f9a6..2f5ea3e151 100644 --- a/pkg/graphql/execution_engine_v2_test.go +++ b/pkg/graphql/execution_engine_v2_test.go @@ -154,7 +154,7 @@ type ExecutionEngineV2TestCase struct { } func TestExecutionEngineV2_Execute(t *testing.T) { - run := func(testCase ExecutionEngineV2TestCase, withError bool) func(t *testing.T) { + run := func(testCase ExecutionEngineV2TestCase, withError bool, expectedErrorMessage string) func(t *testing.T) { t.Helper() return func(t *testing.T) { @@ -191,6 +191,9 @@ func TestExecutionEngineV2_Execute(t *testing.T) { if withError { assert.Error(t, err) + if expectedErrorMessage != "" { + assert.Contains(t, err.Error(), expectedErrorMessage) + } } else { assert.NoError(t, err) } @@ -198,11 +201,15 @@ func TestExecutionEngineV2_Execute(t *testing.T) { } runWithError := func(testCase ExecutionEngineV2TestCase) func(t *testing.T) { - return run(testCase, true) + return run(testCase, true, "") + } + + runWithAndCompareError := func(testCase ExecutionEngineV2TestCase, expectedErrorMessage string) func(t *testing.T) { + return run(testCase, true, expectedErrorMessage) } runWithoutError := func(testCase ExecutionEngineV2TestCase) func(t *testing.T) { - return run(testCase, false) + return run(testCase, false, "") } t.Run("execute with empty request object should not panic", runWithError( @@ -408,7 +415,7 @@ func TestExecutionEngineV2_Execute(t *testing.T) { `, } }, - expectedResponse: `{"data":{"q":{"name":"Query","kind":"OBJECT","fields":[{"name":"droid"},{"name":"search"}]},"h":{"name":"Human","fields":[{"name":"name"},{"name":"friends"}]}}}`, + expectedResponse: `{"data":{"q":{"name":"Query","kind":"OBJECT","fields":[{"name":"droid"},{"name":"search"},{"name":"searchResults"}]},"h":{"name":"Human","fields":[{"name":"name"},{"name":"friends"}]}}}`, }, )) @@ -452,7 +459,7 @@ func TestExecutionEngineV2_Execute(t *testing.T) { }`, } }, - expectedResponse: `{"data":{"__type":{"name":"Query","kind":"OBJECT","fields":[{"name":"hero"},{"name":"droid"},{"name":"search"}]}}}`, + expectedResponse: `{"data":{"__type":{"name":"Query","kind":"OBJECT","fields":[{"name":"hero"},{"name":"droid"},{"name":"search"},{"name":"searchResults"}]}}}`, }, )) @@ -462,7 +469,7 @@ func TestExecutionEngineV2_Execute(t *testing.T) { operation: func(t *testing.T) Request { return requestForQuery(t, starwars.FileIntrospectionQuery) }, - expectedResponse: `{"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":{"name":"Mutation"},"subscriptionType":{"name":"Subscription"},"types":[{"kind":"UNION","name":"SearchResult","description":"","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Human","ofType":null},{"kind":"OBJECT","name":"Droid","ofType":null},{"kind":"OBJECT","name":"Starship","ofType":null}]},{"kind":"OBJECT","name":"Query","description":"","fields":[{"name":"hero","description":"","args":[],"type":{"kind":"INTERFACE","name":"Character","ofType":null},"isDeprecated":true,"deprecationReason":"No longer supported"},{"name":"droid","description":"","args":[{"name":"id","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Droid","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"search","description":"","args":[{"name":"name","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"UNION","name":"SearchResult","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"OBJECT","name":"Mutation","description":"","fields":[{"name":"createReview","description":"","args":[{"name":"episode","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Episode","ofType":null}},"defaultValue":null},{"name":"review","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"ReviewInput","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Review","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"OBJECT","name":"Subscription","description":"","fields":[{"name":"remainingJedis","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"INPUT_OBJECT","name":"ReviewInput","description":"","fields":null,"inputFields":[{"name":"stars","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"defaultValue":null},{"name":"commentary","description":"","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"OBJECT","name":"Review","description":"","fields":[{"name":"id","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"stars","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"commentary","description":"","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"ENUM","name":"Episode","description":"","fields":null,"inputFields":[],"interfaces":[],"enumValues":[{"name":"NEWHOPE","description":"","isDeprecated":false,"deprecationReason":null},{"name":"EMPIRE","description":"","isDeprecated":false,"deprecationReason":null},{"name":"JEDI","description":"","isDeprecated":true,"deprecationReason":"No longer supported"}],"possibleTypes":[]},{"kind":"INTERFACE","name":"Character","description":"","fields":[{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"friends","description":"","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"INTERFACE","name":"Character","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Human","ofType":null},{"kind":"OBJECT","name":"Droid","ofType":null}]},{"kind":"OBJECT","name":"Human","description":"","fields":[{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"height","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":true,"deprecationReason":"No longer supported"},{"name":"friends","description":"","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"INTERFACE","name":"Character","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[{"kind":"INTERFACE","name":"Character","ofType":null}],"enumValues":null,"possibleTypes":[]},{"kind":"OBJECT","name":"Droid","description":"","fields":[{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"primaryFunction","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"friends","description":"","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"INTERFACE","name":"Character","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[{"kind":"INTERFACE","name":"Character","ofType":null}],"enumValues":null,"possibleTypes":[]},{"kind":"OBJECT","name":"Starship","description":"","fields":[{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"length","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Float","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"Int","description":"The 'Int' scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"Float","description":"The 'Float' scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"String","description":"The 'String' scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"Boolean","description":"The 'Boolean' scalar type represents 'true' or 'false' .","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"ID","description":"The 'ID' scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as '4') or integer (such as 4) input value will be accepted as an ID.","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]}],"directives":[{"name":"include","description":"Directs the executor to include this field or fragment only when the argument is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Included when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"skip","description":"Directs the executor to skip this field or fragment when the argument is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Skipped when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"deprecated","description":"Marks an element of a GraphQL schema as no longer supported.","locations":["FIELD_DEFINITION","ENUM_VALUE"],"args":[{"name":"reason","description":"Explains why this element was deprecated, usually also including a suggestion\n for how to access supported similar data. Formatted in\n [Markdown](https://daringfireball.net/projects/markdown/).","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":"\"No longer supported\""}]},{"name":"removeNullVariables","description":"The @removeNullVariables directive allows you to remove variables with null value from your GraphQL Query or Mutation Operations.\n\nA potential use-case could be that you have a graphql upstream which is not accepting null values for variables.\nBy enabling this directive all variables with null values will be removed from upstream query.\n\nquery ($say: String, $name: String) @removeNullVariables {\n\thello(say: $say, name: $name)\n}\n\nDirective will transform variables json and remove top level null values.\n{ \"say\": null, \"name\": \"world\" }\n\nSo upstream will receive the following variables:\n\n{ \"name\": \"world\" }","locations":["QUERY","MUTATION"],"args":[]}]}}}`, + expectedResponse: `{"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":{"name":"Mutation"},"subscriptionType":{"name":"Subscription"},"types":[{"kind":"UNION","name":"SearchResult","description":"","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Human","ofType":null},{"kind":"OBJECT","name":"Droid","ofType":null},{"kind":"OBJECT","name":"Starship","ofType":null}]},{"kind":"OBJECT","name":"Query","description":"","fields":[{"name":"hero","description":"","args":[],"type":{"kind":"INTERFACE","name":"Character","ofType":null},"isDeprecated":true,"deprecationReason":"No longer supported"},{"name":"droid","description":"","args":[{"name":"id","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Droid","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"search","description":"","args":[{"name":"name","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"UNION","name":"SearchResult","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"searchResults","description":"","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"UNION","name":"SearchResult","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"OBJECT","name":"Mutation","description":"","fields":[{"name":"createReview","description":"","args":[{"name":"episode","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Episode","ofType":null}},"defaultValue":null},{"name":"review","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"INPUT_OBJECT","name":"ReviewInput","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Review","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"OBJECT","name":"Subscription","description":"","fields":[{"name":"remainingJedis","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"INPUT_OBJECT","name":"ReviewInput","description":"","fields":null,"inputFields":[{"name":"stars","description":"","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"defaultValue":null},{"name":"commentary","description":"","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null}],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"OBJECT","name":"Review","description":"","fields":[{"name":"id","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"stars","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Int","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"commentary","description":"","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"ENUM","name":"Episode","description":"","fields":null,"inputFields":[],"interfaces":[],"enumValues":[{"name":"NEWHOPE","description":"","isDeprecated":false,"deprecationReason":null},{"name":"EMPIRE","description":"","isDeprecated":false,"deprecationReason":null},{"name":"JEDI","description":"","isDeprecated":true,"deprecationReason":"No longer supported"}],"possibleTypes":[]},{"kind":"INTERFACE","name":"Character","description":"","fields":[{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"friends","description":"","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"INTERFACE","name":"Character","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Human","ofType":null},{"kind":"OBJECT","name":"Droid","ofType":null}]},{"kind":"OBJECT","name":"Human","description":"","fields":[{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"height","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":true,"deprecationReason":"No longer supported"},{"name":"friends","description":"","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"INTERFACE","name":"Character","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[{"kind":"INTERFACE","name":"Character","ofType":null}],"enumValues":null,"possibleTypes":[]},{"kind":"OBJECT","name":"Droid","description":"","fields":[{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"primaryFunction","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"friends","description":"","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"INTERFACE","name":"Character","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[{"kind":"INTERFACE","name":"Character","ofType":null}],"enumValues":null,"possibleTypes":[]},{"kind":"INTERFACE","name":"Vehicle","description":"","fields":[{"name":"length","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Float","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Starship","ofType":null}]},{"kind":"OBJECT","name":"Starship","description":"","fields":[{"name":"name","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"length","description":"","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Float","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":[],"interfaces":[{"kind":"INTERFACE","name":"Vehicle","ofType":null}],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"Int","description":"The 'Int' scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"Float","description":"The 'Float' scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point).","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"String","description":"The 'String' scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"Boolean","description":"The 'Boolean' scalar type represents 'true' or 'false' .","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]},{"kind":"SCALAR","name":"ID","description":"The 'ID' scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as '4') or integer (such as 4) input value will be accepted as an ID.","fields":null,"inputFields":[],"interfaces":[],"enumValues":null,"possibleTypes":[]}],"directives":[{"name":"include","description":"Directs the executor to include this field or fragment only when the argument is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Included when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"skip","description":"Directs the executor to skip this field or fragment when the argument is true.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":"Skipped when true.","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"deprecated","description":"Marks an element of a GraphQL schema as no longer supported.","locations":["FIELD_DEFINITION","ENUM_VALUE"],"args":[{"name":"reason","description":"Explains why this element was deprecated, usually also including a suggestion\n for how to access supported similar data. Formatted in\n [Markdown](https://daringfireball.net/projects/markdown/).","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":"\"No longer supported\""}]},{"name":"removeNullVariables","description":"The @removeNullVariables directive allows you to remove variables with null value from your GraphQL Query or Mutation Operations.\n\nA potential use-case could be that you have a graphql upstream which is not accepting null values for variables.\nBy enabling this directive all variables with null values will be removed from upstream query.\n\nquery ($say: String, $name: String) @removeNullVariables {\n\thello(say: $say, name: $name)\n}\n\nDirective will transform variables json and remove top level null values.\n{ \"say\": null, \"name\": \"world\" }\n\nSo upstream will receive the following variables:\n\n{ \"name\": \"world\" }","locations":["QUERY","MUTATION"],"args":[]}]}}}`, }, )) }) @@ -1430,7 +1437,7 @@ func TestExecutionEngineV2_Execute(t *testing.T) { HTTPClient: testNetHttpClient(t, roundTripperTestCase{ expectedHost: "example.com", expectedPath: "/", - expectedBody: `{"query":"{codeType {__typename code ... on Country {name}}}"}`, + expectedBody: `{"query":"{codeType {code __typename ... on Country {name}}}"}`, sendResponseBody: `{"data":{"codeType":{"__typename":"Country","code":"de","name":"Germany"}}}`, sendStatusCode: 200, }), @@ -1724,6 +1731,107 @@ func TestExecutionEngineV2_Execute(t *testing.T) { expectedResponse: `{"data":{"hero":"Human"}}`, }, )) + + t.Run("Spreading a fragment on an invalid type returns ErrInvalidFragmentSpread", runWithAndCompareError( + ExecutionEngineV2TestCase{ + schema: starwarsSchema(t), + operation: loadStarWarsQuery(starwars.FileInvalidFragmentsQuery, nil), + dataSources: []plan.DataSourceConfiguration{ + { + RootNodes: []plan.TypeField{ + { + TypeName: "Query", + FieldNames: []string{"droid"}, + }, + }, + ChildNodes: []plan.TypeField{ + { + TypeName: "Droid", + FieldNames: []string{"name"}, + }, + }, + Factory: &graphql_datasource.Factory{ + HTTPClient: testNetHttpClient(t, roundTripperTestCase{ + expectedHost: "example.com", + expectedPath: "/", + expectedBody: "", + sendResponseBody: `{"data":{"droid":{"name":"R2D2"}}}`, + sendStatusCode: 200, + }), + }, + Custom: graphql_datasource.ConfigJson(graphql_datasource.Configuration{ + Fetch: graphql_datasource.FetchConfiguration{ + URL: "https://example.com/", + Method: "GET", + }, + }), + }, + }, + fields: []plan.FieldConfiguration{ + { + TypeName: "Query", + FieldName: "droid", + Arguments: []plan.ArgumentConfiguration{ + { + Name: "id", + SourceType: plan.FieldArgumentSource, + RenderConfig: plan.RenderArgumentAsGraphQLValue, + }, + }, + }, + }, + expectedResponse: ``, + }, + "fragment spread: fragment reviewFields must be spread on type Review and not type Droid", + )) + + t.Run("execute the correct operation when sending multiple queries", runWithoutError( + ExecutionEngineV2TestCase{ + schema: starwarsSchema(t), + operation: func(t *testing.T) Request { + request := loadStarWarsQuery(starwars.FileInterfaceFragmentsOnUnion, nil)(t) + request.OperationName = "SearchResults" + return request + }, + dataSources: []plan.DataSourceConfiguration{ + { + RootNodes: []plan.TypeField{ + { + TypeName: "Query", + FieldNames: []string{"searchResults"}, + }, + }, + ChildNodes: []plan.TypeField{ + { + TypeName: "Character", + FieldNames: []string{"name"}, + }, + { + TypeName: "Vehicle", + FieldNames: []string{"length"}, + }, + }, + Factory: &graphql_datasource.Factory{ + HTTPClient: testNetHttpClient(t, roundTripperTestCase{ + expectedHost: "example.com", + expectedPath: "/", + expectedBody: "", + sendResponseBody: `{"data":{"searchResults":[{"name"":"Luke Skywalker"},{"length":13.37}]}}`, + sendStatusCode: 200, + }), + }, + Custom: graphql_datasource.ConfigJson(graphql_datasource.Configuration{ + Fetch: graphql_datasource.FetchConfiguration{ + URL: "https://example.com/", + Method: "GET", + }, + }), + }, + }, + fields: []plan.FieldConfiguration{}, + expectedResponse: `{"data":{"searchResults":null}}`, + }, + )) } func TestExecutionEngineV2_FederationAndSubscription_IntegrationTest(t *testing.T) { @@ -1967,7 +2075,7 @@ func TestExecutionWithOptions(t *testing.T) { resultWriter := NewEngineResultWriter() err = engine.Execute(context.Background(), &operation, &resultWriter, WithBeforeFetchHook(before), WithAfterFetchHook(after)) - assert.Equal(t, `{"method":"GET","url":"https://example.com/","body":{"query":"{hero {__typename name}}"}}`, before.input) + assert.Equal(t, `{"method":"GET","url":"https://example.com/","body":{"query":"{hero {name}}"}}`, before.input) assert.Equal(t, `{"hero":{"name":"Luke Skywalker"}}`, after.data) assert.Equal(t, "", after.err) assert.NoError(t, err) diff --git a/pkg/graphqljsonschema/jsonschema.go b/pkg/graphqljsonschema/jsonschema.go index dedd71791d..a67f345731 100644 --- a/pkg/graphqljsonschema/jsonschema.go +++ b/pkg/graphqljsonschema/jsonschema.go @@ -4,10 +4,9 @@ import ( "context" "encoding/json" "fmt" - "strings" "github.com/buger/jsonparser" - "github.com/qri-io/jsonschema" + "github.com/santhosh-tekuri/jsonschema/v5" "github.com/TykTechnologies/graphql-go-tools/pkg/ast" ) @@ -106,7 +105,7 @@ func (r *fromTypeRefResolver) fromTypeRef(operation, definition *ast.Document, t } typeDefinitionNode, ok := definition.Index.FirstNodeByNameStr(name) if !ok { - return nil + return NewAny() } if typeDefinitionNode.Kind == ast.NodeKindEnumTypeDefinition { return NewString(nonNull) @@ -176,7 +175,7 @@ func (r *fromTypeRefResolver) fromTypeRef(operation, definition *ast.Document, t } type Validator struct { - schema jsonschema.Schema + schema *jsonschema.Schema } func NewValidatorFromSchema(schema JsonSchema) (*Validator, error) { @@ -196,30 +195,40 @@ func MustNewValidatorFromSchema(schema JsonSchema) *Validator { } func NewValidatorFromString(schema string) (*Validator, error) { - var validator Validator - err := json.Unmarshal([]byte(schema), &validator.schema) + sch, err := jsonschema.CompileString("schema.json", schema) if err != nil { return nil, err } - return &validator, nil + return &Validator{ + schema: sch, + }, nil } func MustNewValidatorFromString(schema string) *Validator { - var validator Validator - err := json.Unmarshal([]byte(schema), &validator.schema) + validator, err := NewValidatorFromString(schema) if err != nil { panic(err) } - return &validator + return validator +} + +func (v *Validator) Validate(ctx context.Context, inputJSON []byte) error { + var value interface{} + if err := json.Unmarshal(inputJSON, &value); err != nil { + return err + } + if err := v.schema.Validate(value); err != nil { + return err + } + return nil } func TopLevelType(schema string) (jsonparser.ValueType, error) { - var jsonSchema jsonschema.Schema - err := json.Unmarshal([]byte(schema), &jsonSchema) + sch, err := jsonschema.CompileString("schema.json", schema) if err != nil { return jsonparser.Unknown, err } - switch jsonSchema.TopLevelType() { + switch sch.Types[0] { case "boolean": return jsonparser.Boolean, nil case "string": @@ -239,23 +248,6 @@ func TopLevelType(schema string) (jsonparser.ValueType, error) { } } -func (v *Validator) Validate(ctx context.Context, inputJSON []byte) error { - errs, err := v.schema.ValidateBytes(ctx, inputJSON) - if err != nil { - // There was an issue performing the validation itself. Return a - // generic error so the input isn't exposed. - return fmt.Errorf("could not perform validation") - } - if len(errs) > 0 { - messages := make([]string, len(errs)) - for i := range errs { - messages[i] = errs[i].Error() - } - return fmt.Errorf("validation failed: %v", strings.Join(messages, "; ")) - } - return nil -} - type Kind int const ( @@ -295,7 +287,7 @@ type String struct { Type []string `json:"type"` } -func (_ String) Kind() Kind { +func (String) Kind() Kind { return StringKind } @@ -309,7 +301,7 @@ type ID struct { Type []string `json:"type"` } -func (_ ID) Kind() Kind { +func (ID) Kind() Kind { return IDKind } @@ -323,7 +315,7 @@ type Boolean struct { Type []string `json:"type"` } -func (_ Boolean) Kind() Kind { +func (Boolean) Kind() Kind { return BooleanKind } @@ -343,7 +335,7 @@ func NewNumber(nonNull bool) Number { } } -func (_ Number) Kind() Kind { +func (Number) Kind() Kind { return NumberKind } @@ -351,7 +343,7 @@ type Integer struct { Type []string `json:"type"` } -func (_ Integer) Kind() Kind { +func (Integer) Kind() Kind { return IntegerKind } @@ -365,7 +357,7 @@ type Ref struct { Ref string `json:"$ref"` } -func (_ Ref) Kind() Kind { +func (Ref) Kind() Kind { return RefKind } @@ -383,7 +375,7 @@ type Object struct { Defs map[string]JsonSchema `json:"$defs,omitempty"` } -func (_ Object) Kind() Kind { +func (Object) Kind() Kind { return ObjectKind } @@ -410,7 +402,7 @@ type Array struct { Defs map[string]JsonSchema `json:"$defs,omitempty"` } -func (_ Array) Kind() Kind { +func (Array) Kind() Kind { return ArrayKind } diff --git a/pkg/graphqljsonschema/jsonschema_test.go b/pkg/graphqljsonschema/jsonschema_test.go index af6ef4ba18..09102e1dcc 100644 --- a/pkg/graphqljsonschema/jsonschema_test.go +++ b/pkg/graphqljsonschema/jsonschema_test.go @@ -10,6 +10,18 @@ import ( "github.com/TykTechnologies/graphql-go-tools/internal/pkg/unsafeparser" ) +func prettyPrint(s string) string { + var v interface{} + if err := json.Unmarshal([]byte(s), &v); err != nil { + panic(err) + } + pretty, err := json.MarshalIndent(v, " ", " ") + if err != nil { + panic(err) + } + return string(pretty) +} + func runTest(schema, operation, expectedJsonSchema string, valid []string, invalid []string, opts ...Option) func(t *testing.T) { return func(t *testing.T) { definition := unsafeparser.ParseGraphqlDocumentString(schema) @@ -21,7 +33,7 @@ func runTest(schema, operation, expectedJsonSchema string, valid []string, inval jsonSchemaDefinition := FromTypeRef(&operationDoc, &definition, varType, opts...) actualSchema, err := json.Marshal(jsonSchemaDefinition) assert.NoError(t, err) - assert.Equal(t, expectedJsonSchema, string(actualSchema)) + assert.Equal(t, prettyPrint(expectedJsonSchema), prettyPrint(string(actualSchema))) validator, err := NewValidatorFromString(string(actualSchema)) assert.NoError(t, err) @@ -228,7 +240,7 @@ func TestJsonSchema(t *testing.T) { t.Run("complex recursive schema", runTest( complexRecursiveSchema, `query ($input: db_messagesWhereInput){}`, - `{"type":["object","null"],"properties":{"AND":{"$ref":"#/$defs/db_messagesWhereInput"},"NOT":{"$ref":"#/$defs/db_messagesWhereInput"},"OR":{"type":["array","null"],"items":{"$ref":"#/$defs/db_messagesWhereInput"}},"id":{"$ref":"#/$defs/db_IntFilter"},"message":{"$ref":"#/$defs/db_StringFilter"},"payload":{"$ref":"#/$defs/db_JsonFilter"},"user_id":{"$ref":"#/$defs/db_IntFilter"},"users":{"$ref":"#/$defs/db_UsersRelationFilter"}},"additionalProperties":false,"$defs":{"db_DateTimeFilter":{"type":["object","null"],"properties":{"equals":{},"gt":{},"gte":{},"in":{"type":["array","null"],"items":{}},"lt":{},"lte":{},"not":{"$ref":"#/$defs/db_NestedDateTimeFilter"},"notIn":{"type":["array","null"],"items":{}}},"additionalProperties":false},"db_IntFilter":{"type":["object","null"],"properties":{"equals":null,"gt":null,"gte":null,"in":{"type":["array","null"],"items":null},"lt":null,"lte":null,"not":{"$ref":"#/$defs/db_NestedIntFilter"},"notIn":{"type":["array","null"],"items":null}},"additionalProperties":false},"db_JsonFilter":{"type":["object","null"],"properties":{"equals":{"type":["string","null"]},"not":{"type":["string","null"]}},"additionalProperties":false},"db_MessagesListRelationFilter":{"type":["object","null"],"properties":{"every":{"$ref":"#/$defs/db_messagesWhereInput"},"none":{"$ref":"#/$defs/db_messagesWhereInput"},"some":{"$ref":"#/$defs/db_messagesWhereInput"}},"additionalProperties":false},"db_NestedDateTimeFilter":{"type":["object","null"],"properties":{"equals":{},"gt":{},"gte":{},"in":{"type":["array","null"],"items":{}},"lt":{},"lte":{},"not":{"$ref":"#/$defs/db_NestedDateTimeFilter"},"notIn":{"type":["array","null"],"items":{}}},"additionalProperties":false},"db_NestedIntFilter":{"type":["object","null"],"properties":{"equals":null,"gt":null,"gte":null,"in":{"type":["array","null"],"items":null},"lt":null,"lte":null,"not":{"$ref":"#/$defs/db_NestedIntFilter"},"notIn":{"type":["array","null"],"items":null}},"additionalProperties":false},"db_NestedStringFilter":{"type":["object","null"],"properties":{"contains":null,"endsWith":null,"equals":null,"gt":null,"gte":null,"in":{"type":["array","null"],"items":null},"lt":null,"lte":null,"not":{"$ref":"#/$defs/db_NestedStringFilter"},"notIn":{"type":["array","null"],"items":null},"startsWith":null},"additionalProperties":false},"db_StringFilter":{"type":["object","null"],"properties":{"contains":null,"endsWith":null,"equals":null,"gt":null,"gte":null,"in":{"type":["array","null"],"items":null},"lt":null,"lte":null,"mode":{"type":["string","null"]},"not":{"$ref":"#/$defs/db_NestedStringFilter"},"notIn":{"type":["array","null"],"items":null},"startsWith":null},"additionalProperties":false},"db_UsersRelationFilter":{"type":["object","null"],"properties":{"is":{"$ref":"#/$defs/db_usersWhereInput"},"isNot":{"$ref":"#/$defs/db_usersWhereInput"}},"additionalProperties":false},"db_messagesWhereInput":{"type":["object","null"],"properties":{"AND":{"$ref":"#/$defs/db_messagesWhereInput"},"NOT":{"$ref":"#/$defs/db_messagesWhereInput"},"OR":{"type":["array","null"],"items":{"$ref":"#/$defs/db_messagesWhereInput"}},"id":{"$ref":"#/$defs/db_IntFilter"},"message":{"$ref":"#/$defs/db_StringFilter"},"payload":{"$ref":"#/$defs/db_JsonFilter"},"user_id":{"$ref":"#/$defs/db_IntFilter"},"users":{"$ref":"#/$defs/db_UsersRelationFilter"}},"additionalProperties":false},"db_usersWhereInput":{"type":["object","null"],"properties":{"AND":{"$ref":"#/$defs/db_usersWhereInput"},"NOT":{"$ref":"#/$defs/db_usersWhereInput"},"OR":{"type":["array","null"],"items":{"$ref":"#/$defs/db_usersWhereInput"}},"email":{"$ref":"#/$defs/db_StringFilter"},"id":{"$ref":"#/$defs/db_IntFilter"},"lastlogin":{"$ref":"#/$defs/db_DateTimeFilter"},"messages":{"$ref":"#/$defs/db_MessagesListRelationFilter"},"name":{"$ref":"#/$defs/db_StringFilter"},"pet":{"$ref":"#/$defs/db_StringFilter"},"updatedat":{"$ref":"#/$defs/db_DateTimeFilter"}},"additionalProperties":false}}}`, + complexRecursiveSchemaResult, []string{}, []string{}, )) @@ -257,9 +269,17 @@ func TestJsonSchema(t *testing.T) { }, WithPath([]string{"pet", "name"}), )) + t.Run("not defined scalar", runTest( + `input Container { name: MyScalar }`, + `query ($input: Container){}`, + `{"type":["object", "null"], "properties": {"name": {}}, "additionalProperties": false}`, + []string{}, + []string{}, + )) } const complexRecursiveSchema = ` +scalar Int scalar String input db_NestedIntFilter { equals: Int @@ -1069,3 +1089,553 @@ input db_WidgetsInput { items: [db_WidgetInput]! } ` + +const complexRecursiveSchemaResult = ` +{ + "type": [ + "object", + "null" + ], + "properties": { + "AND": { + "$ref": "#/$defs/db_messagesWhereInput" + }, + "NOT": { + "$ref": "#/$defs/db_messagesWhereInput" + }, + "OR": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/db_messagesWhereInput" + } + }, + "id": { + "$ref": "#/$defs/db_IntFilter" + }, + "message": { + "$ref": "#/$defs/db_StringFilter" + }, + "payload": { + "$ref": "#/$defs/db_JsonFilter" + }, + "user_id": { + "$ref": "#/$defs/db_IntFilter" + }, + "users": { + "$ref": "#/$defs/db_UsersRelationFilter" + } + }, + "additionalProperties": false, + "$defs": { + "db_DateTimeFilter": { + "type": [ + "object", + "null" + ], + "properties": { + "equals": {}, + "gt": {}, + "gte": {}, + "in": { + "type": [ + "array", + "null" + ], + "items": {} + }, + "lt": {}, + "lte": {}, + "not": { + "$ref": "#/$defs/db_NestedDateTimeFilter" + }, + "notIn": { + "type": [ + "array", + "null" + ], + "items": {} + } + }, + "additionalProperties": false + }, + "db_IntFilter": { + "type": [ + "object", + "null" + ], + "properties": { + "equals": { + "type": [ + "integer", + "null" + ] + }, + "gt": { + "type": [ + "integer", + "null" + ] + }, + "gte": { + "type": [ + "integer", + "null" + ] + }, + "in": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "integer", + "null" + ] + } + }, + "lt": { + "type": [ + "integer", + "null" + ] + }, + "lte": { + "type": [ + "integer", + "null" + ] + }, + "not": { + "$ref": "#/$defs/db_NestedIntFilter" + }, + "notIn": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "integer", + "null" + ] + } + } + }, + "additionalProperties": false + }, + "db_JsonFilter": { + "type": [ + "object", + "null" + ], + "properties": { + "equals": { + "type": [ + "string", + "null" + ] + }, + "not": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "db_MessagesListRelationFilter": { + "type": [ + "object", + "null" + ], + "properties": { + "every": { + "$ref": "#/$defs/db_messagesWhereInput" + }, + "none": { + "$ref": "#/$defs/db_messagesWhereInput" + }, + "some": { + "$ref": "#/$defs/db_messagesWhereInput" + } + }, + "additionalProperties": false + }, + "db_NestedDateTimeFilter": { + "type": [ + "object", + "null" + ], + "properties": { + "equals": {}, + "gt": {}, + "gte": {}, + "in": { + "type": [ + "array", + "null" + ], + "items": {} + }, + "lt": {}, + "lte": {}, + "not": { + "$ref": "#/$defs/db_NestedDateTimeFilter" + }, + "notIn": { + "type": [ + "array", + "null" + ], + "items": {} + } + }, + "additionalProperties": false + }, + "db_NestedIntFilter": { + "type": [ + "object", + "null" + ], + "properties": { + "equals": { + "type": [ + "integer", + "null" + ] + }, + "gt": { + "type": [ + "integer", + "null" + ] + }, + "gte": { + "type": [ + "integer", + "null" + ] + }, + "in": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "integer", + "null" + ] + } + }, + "lt": { + "type": [ + "integer", + "null" + ] + }, + "lte": { + "type": [ + "integer", + "null" + ] + }, + "not": { + "$ref": "#/$defs/db_NestedIntFilter" + }, + "notIn": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "integer", + "null" + ] + } + } + }, + "additionalProperties": false + }, + "db_NestedStringFilter": { + "type": [ + "object", + "null" + ], + "properties": { + "contains": { + "type": [ + "string", + "null" + ] + }, + "endsWith": { + "type": [ + "string", + "null" + ] + }, + "equals": { + "type": [ + "string", + "null" + ] + }, + "gt": { + "type": [ + "string", + "null" + ] + }, + "gte": { + "type": [ + "string", + "null" + ] + }, + "in": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "lt": { + "type": [ + "string", + "null" + ] + }, + "lte": { + "type": [ + "string", + "null" + ] + }, + "not": { + "$ref": "#/$defs/db_NestedStringFilter" + }, + "notIn": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "startsWith": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "db_StringFilter": { + "type": [ + "object", + "null" + ], + "properties": { + "contains": { + "type": [ + "string", + "null" + ] + }, + "endsWith": { + "type": [ + "string", + "null" + ] + }, + "equals": { + "type": [ + "string", + "null" + ] + }, + "gt": { + "type": [ + "string", + "null" + ] + }, + "gte": { + "type": [ + "string", + "null" + ] + }, + "in": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "lt": { + "type": [ + "string", + "null" + ] + }, + "lte": { + "type": [ + "string", + "null" + ] + }, + "mode": { + "type": [ + "string", + "null" + ] + }, + "not": { + "$ref": "#/$defs/db_NestedStringFilter" + }, + "notIn": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "startsWith": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "db_UsersRelationFilter": { + "type": [ + "object", + "null" + ], + "properties": { + "is": { + "$ref": "#/$defs/db_usersWhereInput" + }, + "isNot": { + "$ref": "#/$defs/db_usersWhereInput" + } + }, + "additionalProperties": false + }, + "db_messagesWhereInput": { + "type": [ + "object", + "null" + ], + "properties": { + "AND": { + "$ref": "#/$defs/db_messagesWhereInput" + }, + "NOT": { + "$ref": "#/$defs/db_messagesWhereInput" + }, + "OR": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/db_messagesWhereInput" + } + }, + "id": { + "$ref": "#/$defs/db_IntFilter" + }, + "message": { + "$ref": "#/$defs/db_StringFilter" + }, + "payload": { + "$ref": "#/$defs/db_JsonFilter" + }, + "user_id": { + "$ref": "#/$defs/db_IntFilter" + }, + "users": { + "$ref": "#/$defs/db_UsersRelationFilter" + } + }, + "additionalProperties": false + }, + "db_usersWhereInput": { + "type": [ + "object", + "null" + ], + "properties": { + "AND": { + "$ref": "#/$defs/db_usersWhereInput" + }, + "NOT": { + "$ref": "#/$defs/db_usersWhereInput" + }, + "OR": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/db_usersWhereInput" + } + }, + "email": { + "$ref": "#/$defs/db_StringFilter" + }, + "id": { + "$ref": "#/$defs/db_IntFilter" + }, + "lastlogin": { + "$ref": "#/$defs/db_DateTimeFilter" + }, + "messages": { + "$ref": "#/$defs/db_MessagesListRelationFilter" + }, + "name": { + "$ref": "#/$defs/db_StringFilter" + }, + "pet": { + "$ref": "#/$defs/db_StringFilter" + }, + "updatedat": { + "$ref": "#/$defs/db_DateTimeFilter" + } + }, + "additionalProperties": false + } + } +} +` diff --git a/pkg/operationreport/externalerror.go b/pkg/operationreport/externalerror.go index d7fb2497a3..1764857e36 100644 --- a/pkg/operationreport/externalerror.go +++ b/pkg/operationreport/externalerror.go @@ -352,6 +352,14 @@ func ErrFragmentSpreadFormsCycle(spreadName ast.ByteSlice) (err ExternalError) { return err } +func ErrInvalidFragmentSpread(fragmentName, fragmentTypeName, enclosingName ast.ByteSlice) (err ExternalError) { + err.Message = fmt.Sprintf( + "fragment spread: fragment %s must be spread on type %s and not type %s", + fragmentName, fragmentTypeName, enclosingName, + ) + return err +} + func ErrFragmentDefinedButNotUsed(fragmentName ast.ByteSlice) (err ExternalError) { err.Message = fmt.Sprintf("fragment: %s defined but not used", fragmentName) return err diff --git a/pkg/starwars/starwars.go b/pkg/starwars/starwars.go index 09015ebb82..a1115262a7 100644 --- a/pkg/starwars/starwars.go +++ b/pkg/starwars/starwars.go @@ -32,6 +32,8 @@ const ( FileMultiQueries = "testdata/queries/multi_queries.query" FileMultiQueriesWithArguments = "testdata/queries/multi_queries_with_arguments.query" FileInvalidQuery = "testdata/queries/invalid.query" + FileInvalidFragmentsQuery = "testdata/queries/invalid_fragments.query" + FileInterfaceFragmentsOnUnion = "testdata/queries/interface_fragments_on_union.graphql" ) var ( diff --git a/pkg/starwars/testdata/queries/interface_fragments_on_union.graphql b/pkg/starwars/testdata/queries/interface_fragments_on_union.graphql new file mode 100644 index 0000000000..db3162de20 --- /dev/null +++ b/pkg/starwars/testdata/queries/interface_fragments_on_union.graphql @@ -0,0 +1,10 @@ +query SearchResults { + searchResults { + ...on Character { + name + } + ...on Vehicle { + length + } + } +} \ No newline at end of file diff --git a/pkg/starwars/testdata/queries/invalid_fragments.query b/pkg/starwars/testdata/queries/invalid_fragments.query new file mode 100644 index 0000000000..e3e9f26dcc --- /dev/null +++ b/pkg/starwars/testdata/queries/invalid_fragments.query @@ -0,0 +1,11 @@ +query Fragments($droidID: ID!){ + droid(id: $droidID) { + ...reviewFields + } +} + +fragment reviewFields on Review { + id + stars + commentary +} \ No newline at end of file diff --git a/pkg/starwars/testdata/star_wars.graphql b/pkg/starwars/testdata/star_wars.graphql index f2be59b536..8a63a06921 100644 --- a/pkg/starwars/testdata/star_wars.graphql +++ b/pkg/starwars/testdata/star_wars.graphql @@ -10,6 +10,7 @@ type Query { hero: Character @deprecated droid(id: ID!): Droid search(name: String!): SearchResult + searchResults: [SearchResult] } type Mutation { @@ -54,7 +55,11 @@ type Droid implements Character { friends: [Character] } -type Starship { +interface Vehicle { + length: Float! +} + +type Starship implements Vehicle { name: String! length: Float! } diff --git a/pkg/testing/federationtesting/accounts/graph/entity.resolvers.go b/pkg/testing/federationtesting/accounts/graph/entity.resolvers.go index 564bdc3c31..b1a3175e31 100644 --- a/pkg/testing/federationtesting/accounts/graph/entity.resolvers.go +++ b/pkg/testing/federationtesting/accounts/graph/entity.resolvers.go @@ -2,6 +2,7 @@ package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.22 import ( "context" diff --git a/pkg/testing/federationtesting/accounts/graph/generated/generated.go b/pkg/testing/federationtesting/accounts/graph/generated/generated.go index ab51396f4b..9b96a55cd4 100644 --- a/pkg/testing/federationtesting/accounts/graph/generated/generated.go +++ b/pkg/testing/federationtesting/accounts/graph/generated/generated.go @@ -54,19 +54,23 @@ type ComplexityRoot struct { } Purchase struct { - Product func(childComplexity int) int - Wallet func(childComplexity int) int + Product func(childComplexity int) int + Quantity func(childComplexity int) int + Wallet func(childComplexity int) int } Query struct { + Histories func(childComplexity int) int + Identifiable func(childComplexity int) int Me func(childComplexity int) int __resolve__service func(childComplexity int) int __resolve_entities func(childComplexity int, representations []map[string]interface{}) int } Sale struct { - Product func(childComplexity int) int - Rating func(childComplexity int) int + Location func(childComplexity int) int + Product func(childComplexity int) int + Rating func(childComplexity int) int } User struct { @@ -97,6 +101,8 @@ type EntityResolver interface { } type QueryResolver interface { Me(ctx context.Context) (*model.User, error) + Identifiable(ctx context.Context) (model.Identifiable, error) + Histories(ctx context.Context) ([]model.History, error) } type executableSchema struct { @@ -140,6 +146,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Purchase.Product(childComplexity), true + case "Purchase.quantity": + if e.complexity.Purchase.Quantity == nil { + break + } + + return e.complexity.Purchase.Quantity(childComplexity), true + case "Purchase.wallet": if e.complexity.Purchase.Wallet == nil { break @@ -147,6 +160,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Purchase.Wallet(childComplexity), true + case "Query.histories": + if e.complexity.Query.Histories == nil { + break + } + + return e.complexity.Query.Histories(childComplexity), true + + case "Query.identifiable": + if e.complexity.Query.Identifiable == nil { + break + } + + return e.complexity.Query.Identifiable(childComplexity), true + case "Query.me": if e.complexity.Query.Me == nil { break @@ -173,6 +200,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.__resolve_entities(childComplexity, args["representations"].([]map[string]interface{})), true + case "Sale.location": + if e.complexity.Sale.Location == nil { + break + } + + return e.complexity.Sale.Location(childComplexity), true + case "Sale.product": if e.complexity.Sale.Product == nil { break @@ -311,9 +345,15 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er var sources = []*ast.Source{ {Name: "../schema.graphqls", Input: `extend type Query { me: User + identifiable: Identifiable + histories: [History] } -type User @key(fields: "id") { +interface Identifiable { + id: ID! +} + +type User implements Identifiable @key(fields: "id") { id: ID! username: String! history: [History!]! @@ -325,14 +365,24 @@ extend type Product @key(fields: "upc") { union History = Purchase | Sale -type Purchase { +interface Info { + quantity: Int! +} + +type Purchase implements Info { product: Product! wallet: Wallet + quantity: Int! +} + +interface Store { + location: String! } -type Sale { +type Sale implements Store { product: Product! rating: Int! + location: String! } interface Wallet { @@ -667,6 +717,50 @@ func (ec *executionContext) fieldContext_Purchase_wallet(ctx context.Context, fi return fc, nil } +func (ec *executionContext) _Purchase_quantity(ctx context.Context, field graphql.CollectedField, obj *model.Purchase) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Purchase_quantity(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Quantity, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Purchase_quantity(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Purchase", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_me(ctx, field) if err != nil { @@ -716,6 +810,88 @@ func (ec *executionContext) fieldContext_Query_me(ctx context.Context, field gra return fc, nil } +func (ec *executionContext) _Query_identifiable(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_identifiable(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Identifiable(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(model.Identifiable) + fc.Result = res + return ec.marshalOIdentifiable2githubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐIdentifiable(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_identifiable(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE") + }, + } + return fc, nil +} + +func (ec *executionContext) _Query_histories(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_histories(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Histories(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]model.History) + fc.Result = res + return ec.marshalOHistory2ᚕgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐHistory(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_histories(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type History does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Query__entities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query__entities(ctx, field) if err != nil { @@ -1040,6 +1216,50 @@ func (ec *executionContext) fieldContext_Sale_rating(ctx context.Context, field return fc, nil } +func (ec *executionContext) _Sale_location(ctx context.Context, field graphql.CollectedField, obj *model.Sale) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Sale_location(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Location, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Sale_location(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Sale", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { fc, err := ec.fieldContext_User_id(ctx, field) if err != nil { @@ -3277,6 +3497,54 @@ func (ec *executionContext) _History(ctx context.Context, sel ast.SelectionSet, } } +func (ec *executionContext) _Identifiable(ctx context.Context, sel ast.SelectionSet, obj model.Identifiable) graphql.Marshaler { + switch obj := (obj).(type) { + case nil: + return graphql.Null + case model.User: + return ec._User(ctx, sel, &obj) + case *model.User: + if obj == nil { + return graphql.Null + } + return ec._User(ctx, sel, obj) + default: + panic(fmt.Errorf("unexpected type %T", obj)) + } +} + +func (ec *executionContext) _Info(ctx context.Context, sel ast.SelectionSet, obj model.Info) graphql.Marshaler { + switch obj := (obj).(type) { + case nil: + return graphql.Null + case model.Purchase: + return ec._Purchase(ctx, sel, &obj) + case *model.Purchase: + if obj == nil { + return graphql.Null + } + return ec._Purchase(ctx, sel, obj) + default: + panic(fmt.Errorf("unexpected type %T", obj)) + } +} + +func (ec *executionContext) _Store(ctx context.Context, sel ast.SelectionSet, obj model.Store) graphql.Marshaler { + switch obj := (obj).(type) { + case nil: + return graphql.Null + case model.Sale: + return ec._Sale(ctx, sel, &obj) + case *model.Sale: + if obj == nil { + return graphql.Null + } + return ec._Sale(ctx, sel, obj) + default: + panic(fmt.Errorf("unexpected type %T", obj)) + } +} + func (ec *executionContext) _Wallet(ctx context.Context, sel ast.SelectionSet, obj model.Wallet) graphql.Marshaler { switch obj := (obj).(type) { case nil: @@ -3408,7 +3676,7 @@ func (ec *executionContext) _Product(ctx context.Context, sel ast.SelectionSet, return out } -var purchaseImplementors = []string{"Purchase", "History"} +var purchaseImplementors = []string{"Purchase", "History", "Info"} func (ec *executionContext) _Purchase(ctx context.Context, sel ast.SelectionSet, obj *model.Purchase) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, purchaseImplementors) @@ -3429,6 +3697,13 @@ func (ec *executionContext) _Purchase(ctx context.Context, sel ast.SelectionSet, out.Values[i] = ec._Purchase_wallet(ctx, field, obj) + case "quantity": + + out.Values[i] = ec._Purchase_quantity(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -3476,6 +3751,46 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) } + out.Concurrently(i, func() graphql.Marshaler { + return rrm(innerCtx) + }) + case "identifiable": + field := field + + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_identifiable(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) + } + + out.Concurrently(i, func() graphql.Marshaler { + return rrm(innerCtx) + }) + case "histories": + field := field + + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_histories(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc) + } + out.Concurrently(i, func() graphql.Marshaler { return rrm(innerCtx) }) @@ -3548,7 +3863,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return out } -var saleImplementors = []string{"Sale", "History"} +var saleImplementors = []string{"Sale", "History", "Store"} func (ec *executionContext) _Sale(ctx context.Context, sel ast.SelectionSet, obj *model.Sale) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, saleImplementors) @@ -3569,6 +3884,13 @@ func (ec *executionContext) _Sale(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._Sale_rating(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "location": + + out.Values[i] = ec._Sale_location(ctx, field, obj) + if out.Values[i] == graphql.Null { invalids++ } @@ -3583,7 +3905,7 @@ func (ec *executionContext) _Sale(ctx context.Context, sel ast.SelectionSet, obj return out } -var userImplementors = []string{"User", "_Entity"} +var userImplementors = []string{"User", "Identifiable", "_Entity"} func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *model.User) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, userImplementors) @@ -4594,6 +4916,61 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } +func (ec *executionContext) marshalOHistory2githubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐHistory(ctx context.Context, sel ast.SelectionSet, v model.History) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._History(ctx, sel, v) +} + +func (ec *executionContext) marshalOHistory2ᚕgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐHistory(ctx context.Context, sel ast.SelectionSet, v []model.History) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOHistory2githubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐHistory(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + +func (ec *executionContext) marshalOIdentifiable2githubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋaccountsᚋgraphᚋmodelᚐIdentifiable(ctx context.Context, sel ast.SelectionSet, v model.Identifiable) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Identifiable(ctx, sel, v) +} + func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/testing/federationtesting/accounts/graph/histories.go b/pkg/testing/federationtesting/accounts/graph/histories.go index 5049f276ed..8a77014f55 100644 --- a/pkg/testing/federationtesting/accounts/graph/histories.go +++ b/pkg/testing/federationtesting/accounts/graph/histories.go @@ -6,23 +6,51 @@ import ( var histories = []model.History{ &model.Purchase{ - Product: &model.Product{Upc: "top-1"}, - Wallet: &model.WalletType1{ - Currency: "USD", - Amount: 123, - SpecialField1: "some special value 1", - }, + Product: &model.Product{Upc: "top-1"}, + Wallet: walletOne, + Quantity: 1, }, &model.Sale{ - Product: &model.Product{Upc: "top-2"}, - Rating: 5, + Product: &model.Product{Upc: "top-2"}, + Rating: 5, + Location: "Germany", }, &model.Purchase{ - Product: &model.Product{Upc: "top-3"}, - Wallet: &model.WalletType2{ - Currency: "USD", - Amount: 123, - SpecialField2: "some special value 2", - }, + Product: &model.Product{Upc: "top-3"}, + Wallet: walletTwo, + Quantity: 3, + }, +} + +var allHistories = []model.History{ + &model.Purchase{ + Product: &model.Product{Upc: "top-1"}, + Wallet: walletOne, + Quantity: 1, + }, + &model.Sale{ + Product: &model.Product{Upc: "top-1"}, + Rating: 1, + Location: "Germany", + }, + &model.Purchase{ + Product: &model.Product{Upc: "top-2"}, + Wallet: walletTwo, + Quantity: 2, + }, + &model.Sale{ + Product: &model.Product{Upc: "top-2"}, + Rating: 2, + Location: "UK", + }, + &model.Purchase{ + Product: &model.Product{Upc: "top-3"}, + Wallet: walletTwo, + Quantity: 3, + }, + &model.Sale{ + Product: &model.Product{Upc: "top-3"}, + Rating: 3, + Location: "Ukraine", }, } diff --git a/pkg/testing/federationtesting/accounts/graph/model/models_gen.go b/pkg/testing/federationtesting/accounts/graph/model/models_gen.go index 0239dc8aaf..36bb4f9b45 100644 --- a/pkg/testing/federationtesting/accounts/graph/model/models_gen.go +++ b/pkg/testing/federationtesting/accounts/graph/model/models_gen.go @@ -6,6 +6,21 @@ type History interface { IsHistory() } +type Identifiable interface { + IsIdentifiable() + GetID() string +} + +type Info interface { + IsInfo() + GetQuantity() int +} + +type Store interface { + IsStore() + GetLocation() string +} + type Wallet interface { IsWallet() GetCurrency() string @@ -19,25 +34,36 @@ type Product struct { func (Product) IsEntity() {} type Purchase struct { - Product *Product `json:"product"` - Wallet Wallet `json:"wallet"` + Product *Product `json:"product"` + Wallet Wallet `json:"wallet"` + Quantity int `json:"quantity"` } func (Purchase) IsHistory() {} +func (Purchase) IsInfo() {} +func (this Purchase) GetQuantity() int { return this.Quantity } + type Sale struct { - Product *Product `json:"product"` - Rating int `json:"rating"` + Product *Product `json:"product"` + Rating int `json:"rating"` + Location string `json:"location"` } func (Sale) IsHistory() {} +func (Sale) IsStore() {} +func (this Sale) GetLocation() string { return this.Location } + type User struct { ID string `json:"id"` Username string `json:"username"` History []History `json:"history"` } +func (User) IsIdentifiable() {} +func (this User) GetID() string { return this.ID } + func (User) IsEntity() {} type WalletType1 struct { diff --git a/pkg/testing/federationtesting/accounts/graph/schema.graphqls b/pkg/testing/federationtesting/accounts/graph/schema.graphqls index 32755f779c..3e7df02aa3 100644 --- a/pkg/testing/federationtesting/accounts/graph/schema.graphqls +++ b/pkg/testing/federationtesting/accounts/graph/schema.graphqls @@ -1,8 +1,14 @@ extend type Query { me: User + identifiable: Identifiable + histories: [History] } -type User @key(fields: "id") { +interface Identifiable { + id: ID! +} + +type User implements Identifiable @key(fields: "id") { id: ID! username: String! history: [History!]! @@ -14,14 +20,24 @@ extend type Product @key(fields: "upc") { union History = Purchase | Sale -type Purchase { +interface Info { + quantity: Int! +} + +type Purchase implements Info { product: Product! wallet: Wallet + quantity: Int! +} + +interface Store { + location: String! } -type Sale { +type Sale implements Store { product: Product! rating: Int! + location: String! } interface Wallet { diff --git a/pkg/testing/federationtesting/accounts/graph/schema.resolvers.go b/pkg/testing/federationtesting/accounts/graph/schema.resolvers.go index 5a49ef76a4..91db025f1c 100644 --- a/pkg/testing/federationtesting/accounts/graph/schema.resolvers.go +++ b/pkg/testing/federationtesting/accounts/graph/schema.resolvers.go @@ -2,6 +2,7 @@ package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.22 import ( "context" @@ -19,6 +20,20 @@ func (r *queryResolver) Me(ctx context.Context) (*model.User, error) { }, nil } +// Identifiable is the resolver for the identifiable field. +func (r *queryResolver) Identifiable(ctx context.Context) (model.Identifiable, error) { + return &model.User{ + ID: "1234", + Username: "Me", + History: histories, + }, nil +} + +// Histories is the resolver for the histories field. +func (r *queryResolver) Histories(ctx context.Context) ([]model.History, error) { + return allHistories, nil +} + // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } diff --git a/pkg/testing/federationtesting/accounts/graph/wallets.go b/pkg/testing/federationtesting/accounts/graph/wallets.go new file mode 100644 index 0000000000..3d313b6199 --- /dev/null +++ b/pkg/testing/federationtesting/accounts/graph/wallets.go @@ -0,0 +1,15 @@ +package graph + +import "github.com/TykTechnologies/graphql-go-tools/pkg/testing/federationtesting/accounts/graph/model" + +var walletOne = &model.WalletType1{ + Currency: "USD", + Amount: 123, + SpecialField1: "some special value 1", +} + +var walletTwo = &model.WalletType2{ + Currency: "USD", + Amount: 123, + SpecialField2: "some special value 2", +} diff --git a/pkg/testing/federationtesting/federation_intergation_test.go b/pkg/testing/federationtesting/federation_intergation_test.go deleted file mode 100644 index e2109b06e4..0000000000 --- a/pkg/testing/federationtesting/federation_intergation_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package federationtesting - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "path" - "strings" - "testing" - "time" - - "github.com/jensneuse/abstractlogger" - "github.com/stretchr/testify/assert" - - accounts "github.com/TykTechnologies/graphql-go-tools/pkg/testing/federationtesting/accounts/graph" - "github.com/TykTechnologies/graphql-go-tools/pkg/testing/federationtesting/gateway" - products "github.com/TykTechnologies/graphql-go-tools/pkg/testing/federationtesting/products/graph" - reviews "github.com/TykTechnologies/graphql-go-tools/pkg/testing/federationtesting/reviews/graph" -) - -func newFederationSetup() *federationSetup { - accountUpstreamServer := httptest.NewServer(accounts.GraphQLEndpointHandler(accounts.TestOptions)) - productsUpstreamServer := httptest.NewServer(products.GraphQLEndpointHandler(products.TestOptions)) - reviewsUpstreamServer := httptest.NewServer(reviews.GraphQLEndpointHandler(reviews.TestOptions)) - - httpClient := http.DefaultClient - - poller := gateway.NewDatasource([]gateway.ServiceConfig{ - {Name: "accounts", URL: accountUpstreamServer.URL}, - {Name: "products", URL: productsUpstreamServer.URL, WS: strings.ReplaceAll(productsUpstreamServer.URL, "http:", "ws:")}, - {Name: "reviews", URL: reviewsUpstreamServer.URL}, - }, httpClient) - - gtw := gateway.Handler(abstractlogger.NoopLogger, poller, httpClient) - - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - - poller.Run(ctx) - gatewayServer := httptest.NewServer(gtw) - - return &federationSetup{ - accountsUpstreamServer: accountUpstreamServer, - productsUpstreamServer: productsUpstreamServer, - reviewsUpstreamServer: reviewsUpstreamServer, - gatewayServer: gatewayServer, - } -} - -type federationSetup struct { - accountsUpstreamServer *httptest.Server - productsUpstreamServer *httptest.Server - reviewsUpstreamServer *httptest.Server - gatewayServer *httptest.Server -} - -func (f *federationSetup) close() { - f.accountsUpstreamServer.Close() - f.productsUpstreamServer.Close() - f.reviewsUpstreamServer.Close() - f.gatewayServer.Close() -} - -func TestFederationIntegrationTest(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - setup := newFederationSetup() - defer setup.close() - - gqlClient := NewGraphqlClient(http.DefaultClient) - - t.Run("single upstream query operation", func(t *testing.T) { - resp := gqlClient.Query(ctx, setup.gatewayServer.URL, path.Join("testdata", "queries/single_upstream.query"), nil, t) - assert.Equal(t, `{"data":{"me":{"id":"1234","username":"Me"}}}`, string(resp)) - }) - - t.Run("query spans multiple federated servers", func(t *testing.T) { - resp := gqlClient.Query(ctx, setup.gatewayServer.URL, path.Join("testdata", "queries/multiple_upstream.query"), nil, t) - assert.Equal(t, `{"data":{"topProducts":[{"name":"Trilby","reviews":[{"body":"A highly effective form of birth control.","author":{"username":"Me"}}]},{"name":"Fedora","reviews":[{"body":"Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.","author":{"username":"Me"}}]},{"name":"Boater","reviews":[{"body":"This is the last straw. Hat you will wear. 11/10","author":{"username":"User 7777"}}]}]}}`, string(resp)) - }) - - t.Run("mutation operation with variables", func(t *testing.T) { - resp := gqlClient.Query(ctx, setup.gatewayServer.URL, path.Join("testdata", "mutations/mutation_with_variables.query"), queryVariables{ - "authorID": "3210", - "upc": "top-1", - "review": "This is the last straw. Hat you will wear. 11/10", - }, t) - assert.Equal(t, `{"data":{"addReview":{"body":"This is the last straw. Hat you will wear. 11/10","author":{"username":"User 3210"}}}}`, string(resp)) - }) - - t.Run("union query", func(t *testing.T) { - resp := gqlClient.Query(ctx, setup.gatewayServer.URL, path.Join("testdata", "queries/union.query"), nil, t) - assert.Equal(t, `{"data":{"me":{"username":"Me","history":[{"__typename":"Purchase","wallet":{"amount":123}},{"__typename":"Sale","rating":5},{"__typename":"Purchase","wallet":{"amount":123}}]}}}`, string(resp)) - }) - - t.Run("interface query", func(t *testing.T) { - resp := gqlClient.Query(ctx, setup.gatewayServer.URL, path.Join("testdata", "queries/interface.query"), nil, t) - assert.Equal(t, `{"data":{"me":{"username":"Me","history":[{"wallet":{"amount":123,"specialField1":"some special value 1"}},{"rating":5},{"wallet":{"amount":123,"specialField2":"some special value 2"}}]}}}`, string(resp)) - }) - - t.Run("subscription query through WebSocket transport", func(t *testing.T) { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - wsAddr := strings.ReplaceAll(setup.gatewayServer.URL, "http://", "ws://") - fmt.Println("setup.gatewayServer.URL", wsAddr) - messages := gqlClient.Subscription(ctx, wsAddr, path.Join("testdata", "subscriptions/subscription.query"), queryVariables{ - "upc": "top-1", - }, t) - - assert.Equal(t, `{"id":"1","type":"data","payload":{"data":{"updateProductPrice":{"upc":"top-1","name":"Trilby","price":1}}}}`, string(<-messages)) - assert.Equal(t, `{"id":"1","type":"data","payload":{"data":{"updateProductPrice":{"upc":"top-1","name":"Trilby","price":2}}}}`, string(<-messages)) - }) -} diff --git a/pkg/testing/federationtesting/products/graph/entity.resolvers.go b/pkg/testing/federationtesting/products/graph/entity.resolvers.go index 904314b8f3..77fbb9c54f 100644 --- a/pkg/testing/federationtesting/products/graph/entity.resolvers.go +++ b/pkg/testing/federationtesting/products/graph/entity.resolvers.go @@ -2,6 +2,7 @@ package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.22 import ( "context" diff --git a/pkg/testing/federationtesting/products/graph/products.go b/pkg/testing/federationtesting/products/graph/products.go index e7f4654ed0..a77b35ef49 100644 --- a/pkg/testing/federationtesting/products/graph/products.go +++ b/pkg/testing/federationtesting/products/graph/products.go @@ -4,23 +4,31 @@ import ( "github.com/TykTechnologies/graphql-go-tools/pkg/testing/federationtesting/products/graph/model" ) -var hats = []*model.Product{ - { - Upc: "top-1", - Name: "Trilby", - Price: 11, - InStock: 500, - }, - { - Upc: "top-2", - Name: "Fedora", - Price: 22, - InStock: 1200, - }, - { - Upc: "top-3", - Name: "Boater", - Price: 33, - InStock: 850, - }, +var hats []*model.Product + +func Reset() { + hats = []*model.Product{ + { + Upc: "top-1", + Name: "Trilby", + Price: 11, + InStock: 500, + }, + { + Upc: "top-2", + Name: "Fedora", + Price: 22, + InStock: 1200, + }, + { + Upc: "top-3", + Name: "Boater", + Price: 33, + InStock: 850, + }, + } +} + +func init() { + Reset() } diff --git a/pkg/testing/federationtesting/products/graph/schema.resolvers.go b/pkg/testing/federationtesting/products/graph/schema.resolvers.go index 1d1283bbaa..b1fc784ba8 100644 --- a/pkg/testing/federationtesting/products/graph/schema.resolvers.go +++ b/pkg/testing/federationtesting/products/graph/schema.resolvers.go @@ -2,6 +2,7 @@ package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.22 import ( "context" diff --git a/pkg/testing/federationtesting/reviews/graph/attachments.go b/pkg/testing/federationtesting/reviews/graph/attachments.go new file mode 100644 index 0000000000..c0aa059efa --- /dev/null +++ b/pkg/testing/federationtesting/reviews/graph/attachments.go @@ -0,0 +1,32 @@ +package graph + +import "github.com/TykTechnologies/graphql-go-tools/pkg/testing/federationtesting/reviews/graph/model" + +var attachments = []model.Attachment{ + model.Question{ + Upc: "top-1", + Body: "How do I turn it on?", + }, + model.Question{ + Upc: "top-3", + Body: "Any recommendations for other teacosies?", + }, + model.Rating{ + Upc: "top-2", + Body: "The best hat I have ever bought in my life.", + Score: 5, + }, + model.Rating{ + Upc: "top-3", + Body: "Terrible teacosy!!!", + Score: 0, + }, + model.Video{ + Upc: "top-2", + Size: 13.37, + }, + model.Video{ + Upc: "top-3", + Size: 4.20, + }, +} diff --git a/pkg/testing/federationtesting/reviews/graph/entity.resolvers.go b/pkg/testing/federationtesting/reviews/graph/entity.resolvers.go index 6ced8cdb40..07faf10cf0 100644 --- a/pkg/testing/federationtesting/reviews/graph/entity.resolvers.go +++ b/pkg/testing/federationtesting/reviews/graph/entity.resolvers.go @@ -2,6 +2,7 @@ package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.22 import ( "context" diff --git a/pkg/testing/federationtesting/reviews/graph/generated/generated.go b/pkg/testing/federationtesting/reviews/graph/generated/generated.go index 336efb0d35..9c964cfb0f 100644 --- a/pkg/testing/federationtesting/reviews/graph/generated/generated.go +++ b/pkg/testing/federationtesting/reviews/graph/generated/generated.go @@ -40,6 +40,7 @@ type ResolverRoot interface { Entity() EntityResolver Mutation() MutationResolver Product() ProductResolver + Review() ReviewResolver User() UserResolver } @@ -66,10 +67,22 @@ type ComplexityRoot struct { __resolve_entities func(childComplexity int, representations []map[string]interface{}) int } + Question struct { + Body func(childComplexity int) int + Upc func(childComplexity int) int + } + + Rating struct { + Body func(childComplexity int) int + Score func(childComplexity int) int + Upc func(childComplexity int) int + } + Review struct { - Author func(childComplexity int) int - Body func(childComplexity int) int - Product func(childComplexity int) int + Attachments func(childComplexity int) int + Author func(childComplexity int) int + Body func(childComplexity int) int + Product func(childComplexity int) int } User struct { @@ -78,6 +91,11 @@ type ComplexityRoot struct { Username func(childComplexity int) int } + Video struct { + Size func(childComplexity int) int + Upc func(childComplexity int) int + } + _Service struct { SDL func(childComplexity int) int } @@ -93,6 +111,9 @@ type MutationResolver interface { type ProductResolver interface { Reviews(ctx context.Context, obj *model.Product) ([]*model.Review, error) } +type ReviewResolver interface { + Attachments(ctx context.Context, obj *model.Review) ([]model.Attachment, error) +} type UserResolver interface { Username(ctx context.Context, obj *model.User) (string, error) Reviews(ctx context.Context, obj *model.User) ([]*model.Review, error) @@ -182,6 +203,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.__resolve_entities(childComplexity, args["representations"].([]map[string]interface{})), true + case "Question.body": + if e.complexity.Question.Body == nil { + break + } + + return e.complexity.Question.Body(childComplexity), true + + case "Question.upc": + if e.complexity.Question.Upc == nil { + break + } + + return e.complexity.Question.Upc(childComplexity), true + + case "Rating.body": + if e.complexity.Rating.Body == nil { + break + } + + return e.complexity.Rating.Body(childComplexity), true + + case "Rating.score": + if e.complexity.Rating.Score == nil { + break + } + + return e.complexity.Rating.Score(childComplexity), true + + case "Rating.upc": + if e.complexity.Rating.Upc == nil { + break + } + + return e.complexity.Rating.Upc(childComplexity), true + + case "Review.attachments": + if e.complexity.Review.Attachments == nil { + break + } + + return e.complexity.Review.Attachments(childComplexity), true + case "Review.author": if e.complexity.Review.Author == nil { break @@ -224,6 +287,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.Username(childComplexity), true + case "Video.size": + if e.complexity.Video.Size == nil { + break + } + + return e.complexity.Video.Size(childComplexity), true + + case "Video.upc": + if e.complexity.Video.Upc == nil { + break + } + + return e.complexity.Video.Upc(childComplexity), true + case "_Service.sdl": if e.complexity._Service.SDL == nil { break @@ -298,12 +375,36 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er } var sources = []*ast.Source{ - {Name: "../schema.graphqls", Input: `type Review { + {Name: "../schema.graphqls", Input: `interface Comment { + upc: String! + body: String! +} + +type Review { body: String! author: User! @provides(fields: "username") product: Product! + attachments: [Attachment] +} + +type Question implements Comment { + upc: String! + body: String! +} + +type Rating implements Comment { + upc: String! + body: String! + score: Int! +} + +type Video { + upc: String! + size: Float! } +union Attachment = Question | Rating | Video + extend type User @key(fields: "id") { id: ID! @external username: String! @external @@ -657,6 +758,8 @@ func (ec *executionContext) fieldContext_Mutation_addReview(ctx context.Context, return ec.fieldContext_Review_author(ctx, field) case "product": return ec.fieldContext_Review_product(ctx, field) + case "attachments": + return ec.fieldContext_Review_attachments(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Review", field.Name) }, @@ -761,6 +864,8 @@ func (ec *executionContext) fieldContext_Product_reviews(ctx context.Context, fi return ec.fieldContext_Review_author(ctx, field) case "product": return ec.fieldContext_Review_product(ctx, field) + case "attachments": + return ec.fieldContext_Review_attachments(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Review", field.Name) }, @@ -1000,6 +1105,226 @@ func (ec *executionContext) fieldContext_Query___schema(ctx context.Context, fie return fc, nil } +func (ec *executionContext) _Question_upc(ctx context.Context, field graphql.CollectedField, obj *model.Question) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Question_upc(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Upc, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Question_upc(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Question", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Question_body(ctx context.Context, field graphql.CollectedField, obj *model.Question) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Question_body(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Body, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Question_body(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Question", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Rating_upc(ctx context.Context, field graphql.CollectedField, obj *model.Rating) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Rating_upc(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Upc, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Rating_upc(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Rating", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Rating_body(ctx context.Context, field graphql.CollectedField, obj *model.Rating) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Rating_body(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Body, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Rating_body(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Rating", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Rating_score(ctx context.Context, field graphql.CollectedField, obj *model.Rating) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Rating_score(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Score, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Rating_score(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Rating", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Review_body(ctx context.Context, field graphql.CollectedField, obj *model.Review) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Review_body(ctx, field) if err != nil { @@ -1146,6 +1471,47 @@ func (ec *executionContext) fieldContext_Review_product(ctx context.Context, fie return fc, nil } +func (ec *executionContext) _Review_attachments(ctx context.Context, field graphql.CollectedField, obj *model.Review) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Review_attachments(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Review().Attachments(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]model.Attachment) + fc.Result = res + return ec.marshalOAttachment2ᚕgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋreviewsᚋgraphᚋmodelᚐAttachment(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Review_attachments(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Review", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Attachment does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { fc, err := ec.fieldContext_User_id(ctx, field) if err != nil { @@ -1181,17 +1547,112 @@ func (ec *executionContext) fieldContext_User_id(ctx context.Context, field grap fc = &graphql.FieldContext{ Object: "User", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_username(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.User().Username(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_User_username(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _User_reviews(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_reviews(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.User().Reviews(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*model.Review) + fc.Result = res + return ec.marshalOReview2ᚕᚖgithubᚗcomᚋTykTechnologiesᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋreviewsᚋgraphᚋmodelᚐReview(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_User_reviews(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + Field: field, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ID does not have child fields") + switch field.Name { + case "body": + return ec.fieldContext_Review_body(ctx, field) + case "author": + return ec.fieldContext_Review_author(ctx, field) + case "product": + return ec.fieldContext_Review_product(ctx, field) + case "attachments": + return ec.fieldContext_Review_attachments(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Review", field.Name) }, } return fc, nil } -func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_User_username(ctx, field) +func (ec *executionContext) _Video_upc(ctx context.Context, field graphql.CollectedField, obj *model.Video) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Video_upc(ctx, field) if err != nil { return graphql.Null } @@ -1204,7 +1665,7 @@ func (ec *executionContext) _User_username(ctx context.Context, field graphql.Co }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.User().Username(rctx, obj) + return obj.Upc, nil }) if err != nil { ec.Error(ctx, err) @@ -1221,12 +1682,12 @@ func (ec *executionContext) _User_username(ctx context.Context, field graphql.Co return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_User_username(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Video_upc(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "User", + Object: "Video", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { return nil, errors.New("field of type String does not have child fields") }, @@ -1234,8 +1695,8 @@ func (ec *executionContext) fieldContext_User_username(ctx context.Context, fiel return fc, nil } -func (ec *executionContext) _User_reviews(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_User_reviews(ctx, field) +func (ec *executionContext) _Video_size(ctx context.Context, field graphql.CollectedField, obj *model.Video) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Video_size(ctx, field) if err != nil { return graphql.Null } @@ -1248,36 +1709,31 @@ func (ec *executionContext) _User_reviews(ctx context.Context, field graphql.Col }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.User().Reviews(rctx, obj) + return obj.Size, nil }) if err != nil { ec.Error(ctx, err) return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } - res := resTmp.([]*model.Review) + res := resTmp.(float64) fc.Result = res - return ec.marshalOReview2ᚕᚖgithubᚗcomᚋTykTechnologiesᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋreviewsᚋgraphᚋmodelᚐReview(ctx, field.Selections, res) + return ec.marshalNFloat2float64(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_User_reviews(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Video_size(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "User", + Object: "Video", Field: field, - IsMethod: true, - IsResolver: true, + IsMethod: false, + IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "body": - return ec.fieldContext_Review_body(ctx, field) - case "author": - return ec.fieldContext_Review_author(ctx, field) - case "product": - return ec.fieldContext_Review_product(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type Review", field.Name) + return nil, errors.New("field of type Float does not have child fields") }, } return fc, nil @@ -3101,6 +3557,59 @@ func (ec *executionContext) fieldContext___Type_specifiedByURL(ctx context.Conte // region ************************** interface.gotpl *************************** +func (ec *executionContext) _Attachment(ctx context.Context, sel ast.SelectionSet, obj model.Attachment) graphql.Marshaler { + switch obj := (obj).(type) { + case nil: + return graphql.Null + case model.Question: + return ec._Question(ctx, sel, &obj) + case *model.Question: + if obj == nil { + return graphql.Null + } + return ec._Question(ctx, sel, obj) + case model.Rating: + return ec._Rating(ctx, sel, &obj) + case *model.Rating: + if obj == nil { + return graphql.Null + } + return ec._Rating(ctx, sel, obj) + case model.Video: + return ec._Video(ctx, sel, &obj) + case *model.Video: + if obj == nil { + return graphql.Null + } + return ec._Video(ctx, sel, obj) + default: + panic(fmt.Errorf("unexpected type %T", obj)) + } +} + +func (ec *executionContext) _Comment(ctx context.Context, sel ast.SelectionSet, obj model.Comment) graphql.Marshaler { + switch obj := (obj).(type) { + case nil: + return graphql.Null + case model.Question: + return ec._Question(ctx, sel, &obj) + case *model.Question: + if obj == nil { + return graphql.Null + } + return ec._Question(ctx, sel, obj) + case model.Rating: + return ec._Rating(ctx, sel, &obj) + case *model.Rating: + if obj == nil { + return graphql.Null + } + return ec._Rating(ctx, sel, obj) + default: + panic(fmt.Errorf("unexpected type %T", obj)) + } +} + func (ec *executionContext) __Entity(ctx context.Context, sel ast.SelectionSet, obj fedruntime.Entity) graphql.Marshaler { switch obj := (obj).(type) { case nil: @@ -3376,6 +3885,83 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return out } +var questionImplementors = []string{"Question", "Comment", "Attachment"} + +func (ec *executionContext) _Question(ctx context.Context, sel ast.SelectionSet, obj *model.Question) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, questionImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Question") + case "upc": + + out.Values[i] = ec._Question_upc(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "body": + + out.Values[i] = ec._Question_body(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + +var ratingImplementors = []string{"Rating", "Comment", "Attachment"} + +func (ec *executionContext) _Rating(ctx context.Context, sel ast.SelectionSet, obj *model.Rating) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, ratingImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Rating") + case "upc": + + out.Values[i] = ec._Rating_upc(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "body": + + out.Values[i] = ec._Rating_body(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "score": + + out.Values[i] = ec._Rating_score(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var reviewImplementors = []string{"Review"} func (ec *executionContext) _Review(ctx context.Context, sel ast.SelectionSet, obj *model.Review) graphql.Marshaler { @@ -3391,22 +3977,39 @@ func (ec *executionContext) _Review(ctx context.Context, sel ast.SelectionSet, o out.Values[i] = ec._Review_body(ctx, field, obj) if out.Values[i] == graphql.Null { - invalids++ + atomic.AddUint32(&invalids, 1) } case "author": out.Values[i] = ec._Review_author(ctx, field, obj) if out.Values[i] == graphql.Null { - invalids++ + atomic.AddUint32(&invalids, 1) } case "product": out.Values[i] = ec._Review_product(ctx, field, obj) if out.Values[i] == graphql.Null { - invalids++ + atomic.AddUint32(&invalids, 1) + } + case "attachments": + field := field + + innerFunc := func(ctx context.Context) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Review_attachments(ctx, field, obj) + return res } + + out.Concurrently(i, func() graphql.Marshaler { + return innerFunc(ctx) + + }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -3483,6 +4086,41 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj return out } +var videoImplementors = []string{"Video", "Attachment"} + +func (ec *executionContext) _Video(ctx context.Context, sel ast.SelectionSet, obj *model.Video) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, videoImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Video") + case "upc": + + out.Values[i] = ec._Video_upc(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "size": + + out.Values[i] = ec._Video_size(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var _ServiceImplementors = []string{"_Service"} func (ec *executionContext) __Service(ctx context.Context, sel ast.SelectionSet, obj *fedruntime.Service) graphql.Marshaler { @@ -3841,6 +4479,21 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v interface{}) (float64, error) { + res, err := graphql.UnmarshalFloatContext(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNFloat2float64(ctx context.Context, sel ast.SelectionSet, v float64) graphql.Marshaler { + res := graphql.MarshalFloatContext(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return graphql.WrapContextMarshaler(ctx, res) +} + func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) { res, err := graphql.UnmarshalID(v) return res, graphql.ErrorOnPath(ctx, err) @@ -3856,7 +4509,22 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } -func (ec *executionContext) marshalNProduct2githubᚗcomᚋTykTechnologiesᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋreviewsᚋgraphᚋmodelᚐProduct(ctx context.Context, sel ast.SelectionSet, v model.Product) graphql.Marshaler { +func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) { + res, err := graphql.UnmarshalInt(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + res := graphql.MarshalInt(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) marshalNProduct2githubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋreviewsᚋgraphᚋmodelᚐProduct(ctx context.Context, sel ast.SelectionSet, v model.Product) graphql.Marshaler { return ec._Product(ctx, sel, &v) } @@ -4276,6 +4944,54 @@ func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel a return res } +func (ec *executionContext) marshalOAttachment2githubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋreviewsᚋgraphᚋmodelᚐAttachment(ctx context.Context, sel ast.SelectionSet, v model.Attachment) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Attachment(ctx, sel, v) +} + +func (ec *executionContext) marshalOAttachment2ᚕgithubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋreviewsᚋgraphᚋmodelᚐAttachment(ctx context.Context, sel ast.SelectionSet, v []model.Attachment) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOAttachment2githubᚗcomᚋwundergraphᚋgraphqlᚑgoᚑtoolsᚋpkgᚋtestingᚋfederationtestingᚋreviewsᚋgraphᚋmodelᚐAttachment(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/testing/federationtesting/reviews/graph/model/models_gen.go b/pkg/testing/federationtesting/reviews/graph/model/models_gen.go index 8e0d251dce..9bdad47300 100644 --- a/pkg/testing/federationtesting/reviews/graph/model/models_gen.go +++ b/pkg/testing/federationtesting/reviews/graph/model/models_gen.go @@ -1,3 +1,43 @@ // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package model + +type Attachment interface { + IsAttachment() +} + +type Comment interface { + IsComment() + GetUpc() string + GetBody() string +} + +type Question struct { + Upc string `json:"upc"` + Body string `json:"body"` +} + +func (Question) IsComment() {} +func (this Question) GetUpc() string { return this.Upc } +func (this Question) GetBody() string { return this.Body } + +func (Question) IsAttachment() {} + +type Rating struct { + Upc string `json:"upc"` + Body string `json:"body"` + Score int `json:"score"` +} + +func (Rating) IsComment() {} +func (this Rating) GetUpc() string { return this.Upc } +func (this Rating) GetBody() string { return this.Body } + +func (Rating) IsAttachment() {} + +type Video struct { + Upc string `json:"upc"` + Size float64 `json:"size"` +} + +func (Video) IsAttachment() {} diff --git a/pkg/testing/federationtesting/reviews/graph/schema.graphqls b/pkg/testing/federationtesting/reviews/graph/schema.graphqls index e77fb17753..1aacccf290 100644 --- a/pkg/testing/federationtesting/reviews/graph/schema.graphqls +++ b/pkg/testing/federationtesting/reviews/graph/schema.graphqls @@ -1,9 +1,33 @@ +interface Comment { + upc: String! + body: String! +} + type Review { body: String! author: User! @provides(fields: "username") product: Product! + attachments: [Attachment] +} + +type Question implements Comment { + upc: String! + body: String! } +type Rating implements Comment { + upc: String! + body: String! + score: Int! +} + +type Video { + upc: String! + size: Float! +} + +union Attachment = Question | Rating | Video + extend type User @key(fields: "id") { id: ID! @external username: String! @external diff --git a/pkg/testing/federationtesting/reviews/graph/schema.resolvers.go b/pkg/testing/federationtesting/reviews/graph/schema.resolvers.go index 0d24f2bb60..445d80c48b 100644 --- a/pkg/testing/federationtesting/reviews/graph/schema.resolvers.go +++ b/pkg/testing/federationtesting/reviews/graph/schema.resolvers.go @@ -2,6 +2,7 @@ package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.22 import ( "context" @@ -37,6 +38,30 @@ func (r *productResolver) Reviews(ctx context.Context, obj *model.Product) ([]*m return res, nil } +// Attachments is the resolver for the attachments field. +func (r *reviewResolver) Attachments(ctx context.Context, obj *model.Review) ([]model.Attachment, error) { + var res []model.Attachment + + for _, attachment := range attachments { + switch v := attachment.(type) { + case model.Question: + if v.Upc == obj.Product.Upc { + res = append(res, attachment) + } + case model.Rating: + if v.Upc == obj.Product.Upc { + res = append(res, attachment) + } + case model.Video: + if v.Upc == obj.Product.Upc { + res = append(res, attachment) + } + } + } + + return res, nil +} + // Username is the resolver for the username field. func (r *userResolver) Username(ctx context.Context, obj *model.User) (string, error) { username := fmt.Sprintf("User %s", obj.ID) @@ -65,9 +90,13 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Product returns generated.ProductResolver implementation. func (r *Resolver) Product() generated.ProductResolver { return &productResolver{r} } +// Review returns generated.ReviewResolver implementation. +func (r *Resolver) Review() generated.ReviewResolver { return &reviewResolver{r} } + // User returns generated.UserResolver implementation. func (r *Resolver) User() generated.UserResolver { return &userResolver{r} } type mutationResolver struct{ *Resolver } type productResolver struct{ *Resolver } +type reviewResolver struct{ *Resolver } type userResolver struct{ *Resolver } diff --git a/pkg/testing/federationtesting/testdata/queries/complex_nesting.graphql b/pkg/testing/federationtesting/testdata/queries/complex_nesting.graphql new file mode 100644 index 0000000000..771f8ebba0 --- /dev/null +++ b/pkg/testing/federationtesting/testdata/queries/complex_nesting.graphql @@ -0,0 +1,41 @@ +query ComplexNesting { + me { + id + username + history { + ... on Store { + location + } + ... on Purchase { + wallet { + currency + } + } + ... on Sale { + location + product { + upc + name + } + } + } + reviews { + __typename + attachments { + ... on Comment { + upc + ... on Rating { + body + } + } + ... on Question { + body + } + ... on Video { + upc + size + } + } + } + } +} \ No newline at end of file diff --git a/pkg/testing/federationtesting/testdata/queries/interface_fragment_on_object.graphql b/pkg/testing/federationtesting/testdata/queries/interface_fragment_on_object.graphql new file mode 100644 index 0000000000..80aa738993 --- /dev/null +++ b/pkg/testing/federationtesting/testdata/queries/interface_fragment_on_object.graphql @@ -0,0 +1,10 @@ +query InterfaceFragment { + me { + ... on Identifiable { + id + } + ... on User { + username + } + } +} \ No newline at end of file diff --git a/pkg/testing/federationtesting/testdata/queries/interface_fragments_on_union.graphql b/pkg/testing/federationtesting/testdata/queries/interface_fragments_on_union.graphql new file mode 100644 index 0000000000..84eba5a472 --- /dev/null +++ b/pkg/testing/federationtesting/testdata/queries/interface_fragments_on_union.graphql @@ -0,0 +1,12 @@ +query InterfaceFragmentsOnUnion { + histories { + ... on Store { + __typename + location + } + ... on Info { + __typename + quantity + } + } +} \ No newline at end of file diff --git a/pkg/testing/federationtesting/testdata/queries/multiple_queries.query b/pkg/testing/federationtesting/testdata/queries/multiple_queries.query new file mode 100644 index 0000000000..08555d9e9a --- /dev/null +++ b/pkg/testing/federationtesting/testdata/queries/multiple_queries.query @@ -0,0 +1,12 @@ +query { + topProducts { + __typename + price + upc + } + me { + __typename + id + username + } +} \ No newline at end of file diff --git a/pkg/testing/federationtesting/testdata/queries/multiple_queries_with_nested_fragments.query b/pkg/testing/federationtesting/testdata/queries/multiple_queries_with_nested_fragments.query new file mode 100644 index 0000000000..b28b42959a --- /dev/null +++ b/pkg/testing/federationtesting/testdata/queries/multiple_queries_with_nested_fragments.query @@ -0,0 +1,30 @@ +fragment ProductFragment on Product { + __typename + price + upc +} + +fragment ReviewFragment on Review { + __typename + product { + ...ProductFragment + } +} + +fragment UserFragment on User { + __typename + id + username + reviews { + ...ReviewFragment + } +} + +query { + topProducts { + ...ProductFragment + } + me { + ...UserFragment + } +} diff --git a/pkg/testing/federationtesting/testdata/queries/multiple_queries_with_union_return.query b/pkg/testing/federationtesting/testdata/queries/multiple_queries_with_union_return.query new file mode 100644 index 0000000000..e96f60e2a3 --- /dev/null +++ b/pkg/testing/federationtesting/testdata/queries/multiple_queries_with_union_return.query @@ -0,0 +1,27 @@ +query Histories { + me { + __typename + id + username + } + histories { + __typename + ... on Sale { + product { + __typename + upc + } + rating + } + ... on Purchase { + product { + __typename + upc + } + wallet { + __typename + currency + } + } + } +} \ No newline at end of file diff --git a/pkg/testing/federationtesting/testdata/queries/object_fragment_on_interface.graphql b/pkg/testing/federationtesting/testdata/queries/object_fragment_on_interface.graphql new file mode 100644 index 0000000000..dc08d01a99 --- /dev/null +++ b/pkg/testing/federationtesting/testdata/queries/object_fragment_on_interface.graphql @@ -0,0 +1,9 @@ +query InterfaceResponse { + identifiable { + __typename + id + ... on User { + username + } + } +} \ No newline at end of file diff --git a/pkg/variablevalidator/variablevalidator.go b/pkg/variablevalidator/variablevalidator.go index 6666daa4a7..ff86aff439 100644 --- a/pkg/variablevalidator/variablevalidator.go +++ b/pkg/variablevalidator/variablevalidator.go @@ -5,11 +5,14 @@ import ( "context" "errors" "fmt" + + "github.com/buger/jsonparser" + "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/TykTechnologies/graphql-go-tools/pkg/ast" "github.com/TykTechnologies/graphql-go-tools/pkg/astvisitor" "github.com/TykTechnologies/graphql-go-tools/pkg/graphqljsonschema" "github.com/TykTechnologies/graphql-go-tools/pkg/operationreport" - "github.com/buger/jsonparser" ) type VariableValidator struct { @@ -84,7 +87,13 @@ func (v *validatorVisitor) EnterVariableDefinition(ref int) { return } if err := schemaValidator.Validate(context.Background(), variable); err != nil { - v.StopWithExternalErr(operationreport.ErrVariableValidationFailed(variableName, err.Error(), v.operation.VariableDefinitions[ref].VariableValue.Position)) + message := err.Error() + var validationErr *jsonschema.ValidationError + if errors.As(err, &validationErr) && len(validationErr.Causes) > 0 { + message = validationErr.Causes[0].Message + } + + v.StopWithExternalErr(operationreport.ErrVariableValidationFailed(variableName, message, v.operation.VariableDefinitions[ref].VariableValue.Position)) return } } diff --git a/pkg/variablevalidator/variablevalidator_test.go b/pkg/variablevalidator/variablevalidator_test.go index 421a2af84c..1e8052fccf 100644 --- a/pkg/variablevalidator/variablevalidator_test.go +++ b/pkg/variablevalidator/variablevalidator_test.go @@ -1,12 +1,14 @@ package variablevalidator import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/TykTechnologies/graphql-go-tools/internal/pkg/unsafeparser" "github.com/TykTechnologies/graphql-go-tools/pkg/asttransform" "github.com/TykTechnologies/graphql-go-tools/pkg/operationreport" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" ) const testDefinition = ` @@ -93,7 +95,7 @@ func TestVariableValidator(t *testing.T) { name: "nested input variable", operation: customInputMutation, variables: `{"in":{"optionalField":"test"}}`, - expectedError: `Validation for variable "in" failed: validation failed: /: {"optionalField":"te... "requiredField" value is required`, + expectedError: `Validation for variable "in" failed: missing properties: 'requiredField'`, }, { name: "multiple operation should validate first operation",