Stash box client interface (#751)

* Add gql client generation files
* Update dependencies
* Add stash-box client generation to the makefile
* Move scraped scene object matchers to models
* Add stash-box to scrape with dropdown
* Add scrape scene from fingerprint in UI
This commit is contained in:
WithoutPants
2020-09-17 19:57:18 +10:00
committed by GitHub
parent b0b5621337
commit 7a45943e8e
324 changed files with 34978 additions and 17323 deletions

13
.gqlgenc.yml Normal file
View File

@@ -0,0 +1,13 @@
model:
filename: ./pkg/scraper/stashbox/graphql/generated_models.go
client:
filename: ./pkg/scraper/stashbox/graphql/generated_client.go
models:
Date:
model: github.com/99designs/gqlgen/graphql.String
endpoint:
# This points to stashdb.org currently, but can be directed at any stash-box
# instance. It is used for generation only.
url: https://stashdb.org/graphql
query:
- "./graphql/stash-box/*.graphql"

View File

@@ -63,6 +63,11 @@ generate:
go generate -mod=vendor go generate -mod=vendor
cd ui/v2.5 && yarn run gqlgen cd ui/v2.5 && yarn run gqlgen
# Regenerates stash-box client files
.PHONY: generate-stash-box-client
generate-stash-box-client:
go run -mod=vendor github.com/Yamashou/gqlgenc
# Runs gofmt -w on the project's source code, modifying any files that do not match its style. # Runs gofmt -w on the project's source code, modifying any files that do not match its style.
.PHONY: fmt .PHONY: fmt
fmt: fmt:

14
go.mod
View File

@@ -1,7 +1,8 @@
module github.com/stashapp/stash module github.com/stashapp/stash
require ( require (
github.com/99designs/gqlgen v0.9.0 github.com/99designs/gqlgen v0.12.2
github.com/Yamashou/gqlgenc v0.0.0-20200902035953-4dbef3551953
github.com/antchfx/htmlquery v1.2.3 github.com/antchfx/htmlquery v1.2.3
github.com/bmatcuk/doublestar/v2 v2.0.1 github.com/bmatcuk/doublestar/v2 v2.0.1
github.com/chromedp/cdproto v0.0.0-20200608134039-8a80cdaf865c github.com/chromedp/cdproto v0.0.0-20200608134039-8a80cdaf865c
@@ -14,7 +15,6 @@ require (
github.com/gorilla/sessions v1.2.0 github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/h2non/filetype v1.0.8 github.com/h2non/filetype v1.0.8
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
github.com/jmoiron/sqlx v1.2.0 github.com/jmoiron/sqlx v1.2.0
github.com/json-iterator/go v1.1.9 github.com/json-iterator/go v1.1.9
@@ -27,13 +27,13 @@ require (
github.com/spf13/viper v1.7.0 github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
github.com/tidwall/gjson v1.6.0 github.com/tidwall/gjson v1.6.0
github.com/vektah/gqlparser v1.1.2 github.com/vektah/gqlparser/v2 v2.0.1
github.com/vektra/mockery v1.1.2 // indirect
github.com/vektra/mockery/v2 v2.2.1 github.com/vektra/mockery/v2 v2.2.1
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/image v0.0.0-20190802002840-cff245a6509b golang.org/x/image v0.0.0-20190802002840-cff245a6509b
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 golang.org/x/net v0.0.0-20200822124328-c89045814202
gopkg.in/yaml.v2 v2.2.4 golang.org/x/tools v0.0.0-20200915031644-64986481280e // indirect
gopkg.in/yaml.v2 v2.3.0
) )
replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999 replace git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999

91
go.sum
View File

@@ -18,8 +18,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
github.com/99designs/gqlgen v0.9.0 h1:g1arBPML74Vqv0L3Q+TqIhGXLspV+2MYtRLkBxuZrlE= github.com/99designs/gqlgen v0.12.2 h1:aOdpsiCycFtCnAv8CAI1exnKrIDHMqtMzQoXeTziY4o=
github.com/99designs/gqlgen v0.9.0/go.mod h1:HrrG7ic9EgLPsULxsZh/Ti+p0HNWgR3XRuvnD0pb5KY= github.com/99designs/gqlgen v0.12.2/go.mod h1:7zdGo6ry9u1YBp/qlb2uxSU5Mt2jQKLcBETQiKk+Bxo=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -30,8 +30,14 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/Yamashou/gqlgenc v0.0.0-20200902035953-4dbef3551953 h1:+iPJDL28FxZhEdtJ9qykrMt/oDiOvlzTa0zV06nUcFM=
github.com/Yamashou/gqlgenc v0.0.0-20200902035953-4dbef3551953/go.mod h1:kaTsk10p2hJWwrB2t7vMsk1lXj9KAHaDYRtJQiB+Ick=
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ= github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -44,6 +50,8 @@ github.com/antchfx/xpath v1.1.6 h1:6sVh6hB5T6phw1pFpHRQ+C4bd8sNI+O58flqtg7h0R0=
github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -77,6 +85,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg=
@@ -93,6 +103,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc=
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA= github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
@@ -372,6 +385,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -383,6 +398,7 @@ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@@ -396,8 +412,6 @@ github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
@@ -455,6 +469,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
@@ -482,6 +497,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE= github.com/kshvakov/clickhouse v1.3.5/go.mod h1:DMzX7FxRymoNkVgizH0DWAL8Cur7wHLgx3MUnGwJqpE=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
@@ -510,9 +526,14 @@ github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/markbates/sigtx v1.0.0/go.mod h1:QF1Hv6Ic6Ca6W+T+DL0Y/ypborFKyvUY9HmuCD4VeTc= github.com/markbates/sigtx v1.0.0/go.mod h1:QF1Hv6Ic6Ca6W+T+DL0Y/ypborFKyvUY9HmuCD4VeTc=
github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGfH0DQzY7w= github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGfH0DQzY7w=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
@@ -571,6 +592,8 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -604,7 +627,11 @@ github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8= github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
@@ -612,6 +639,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 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/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
@@ -637,6 +666,7 @@ github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5J
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
@@ -650,7 +680,9 @@ github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
@@ -684,6 +716,7 @@ github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
@@ -701,14 +734,14 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/unrolled/secure v0.0.0-20180918153822-f340ee86eb8b/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA= github.com/unrolled/secure v0.0.0-20180918153822-f340ee86eb8b/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA= github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68= github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74=
github.com/vektra/mockery v1.1.2 h1:uc0Yn67rJpjt8U/mAZimdCKn9AeA97BOkjpmtBSlfP4= github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
github.com/vektra/mockery v1.1.2/go.mod h1:VcfZjKaFOPO+MpN4ZvwPjs4c48lkq1o3Ym8yHZJu0jU= github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
github.com/vektra/mockery/v2 v2.2.1 h1:EYgPvxyYkm/0JKs62qlVc9pO+ljb8biPbDWabk5/PmI= github.com/vektra/mockery/v2 v2.2.1 h1:EYgPvxyYkm/0JKs62qlVc9pO+ljb8biPbDWabk5/PmI=
github.com/vektra/mockery/v2 v2.2.1/go.mod h1:rBZUbbhMbiSX1WlCGsOgAi6xjuJGxB7KKbnoL0XNYW8= github.com/vektra/mockery/v2 v2.2.1/go.mod h1:rBZUbbhMbiSX1WlCGsOgAi6xjuJGxB7KKbnoL0XNYW8=
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
@@ -717,6 +750,8 @@ github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -752,18 +787,16 @@ golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/Le
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2 h1:FNSSV4jv1PrPsiM2iKGpqLPPgYACqh9Muav7Pollk1k=
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -779,8 +812,11 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -812,11 +848,13 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgP
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI=
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -833,6 +871,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -859,6 +899,7 @@ golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0= golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -933,12 +974,24 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/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-20191119224855-298f0cb1881e/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-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e h1:ssd5ulOvVWlh4kDSUF2SqzmMeWfjmwDXM+uGw/aQjRE= golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e h1:ssd5ulOvVWlh4kDSUF2SqzmMeWfjmwDXM+uGw/aQjRE=
golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200323144430-8dcfad9e016e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200827163409-021d7c6f1ec3 h1:OjYQxZBKJFs+sJbHkvSGIKNMkZXDJQ9JsMpebGhkafI=
golang.org/x/tools v0.0.0-20200827163409-021d7c6f1ec3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200915031644-64986481280e h1:tfSNPIxC48Azhz4nLSPskz/yE9R6ftFRK8pfgfqWUAc=
golang.org/x/tools v0.0.0-20200915031644-64986481280e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
@@ -956,6 +1009,7 @@ google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO50
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -984,6 +1038,8 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
@@ -1000,6 +1056,9 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -66,3 +66,9 @@ query ScrapeMovieURL($url: String!) {
...ScrapedMovieData ...ScrapedMovieData
} }
} }
query QueryStashBoxScene($input: StashBoxQueryInput!) {
queryStashBoxScene(input: $input) {
...ScrapedSceneData
}
}

View File

@@ -79,6 +79,9 @@ type Query {
"""Scrape a list of performers from a query""" """Scrape a list of performers from a query"""
scrapeFreeonesPerformerList(query: String!): [String!]! scrapeFreeonesPerformerList(query: String!): [String!]!
"""Query StashBox for scenes"""
queryStashBoxScene(input: StashBoxQueryInput!): [ScrapedScene!]!
# Plugins # Plugins
"""List loaded plugins""" """List loaded plugins"""
plugins: [Plugin!] plugins: [Plugin!]

View File

@@ -24,7 +24,6 @@ type Scraper {
movie: ScraperSpec movie: ScraperSpec
} }
type ScrapedScenePerformer { type ScrapedScenePerformer {
"""Set if performer matched""" """Set if performer matched"""
stored_id: ID stored_id: ID
@@ -88,3 +87,12 @@ type ScrapedScene {
performers: [ScrapedScenePerformer!] performers: [ScrapedScenePerformer!]
movies: [ScrapedSceneMovie!] movies: [ScrapedSceneMovie!]
} }
input StashBoxQueryInput {
"""Index of the configured stash-box instance to use"""
stash_box_index: Int!
"""Instructs query by scene fingerprints"""
scene_ids: [ID!]
"""Query by query string"""
q: String
}

View File

@@ -0,0 +1,139 @@
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
...URLFragment
}
images {
...ImageFragment
}
}
fragment TagFragment on Tag {
name
id
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
urls {
...URLFragment
}
images {
...ImageFragment
}
birthdate {
...FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
...MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
...BodyModificationFragment
}
piercings {
...BodyModificationFragment
}
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
...PerformerFragment
}
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment SceneFragment on Scene {
id
title
details
duration
date
urls {
...URLFragment
}
images {
...ImageFragment
}
studio {
...StudioFragment
}
tags {
...TagFragment
}
performers {
...PerformerAppearanceFragment
}
fingerprints {
...FingerprintFragment
}
}
query FindSceneByFingerprint($fingerprint: FingerprintQueryInput!) {
findSceneByFingerprint(fingerprint: $fingerprint) {
...SceneFragment
}
}
query FindScenesByFingerprints($fingerprints: [String!]!) {
findScenesByFingerprints(fingerprints: $fingerprints) {
...SceneFragment
}
}
query SearchScene($term: String!) {
searchScene(term: $term) {
...SceneFragment
}
}
mutation SubmitFingerprint($input: FingerprintSubmission!) {
submitFingerprint(input: $input)
}

View File

@@ -2,10 +2,13 @@ package api
import ( import (
"context" "context"
"fmt"
"github.com/stashapp/stash/pkg/manager" "github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scraper" "github.com/stashapp/stash/pkg/scraper"
"github.com/stashapp/stash/pkg/scraper/stashbox"
) )
// deprecated // deprecated
@@ -72,3 +75,23 @@ func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*models
func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models.ScrapedMovie, error) { func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models.ScrapedMovie, error) {
return manager.GetInstance().ScraperCache.ScrapeMovieURL(url) return manager.GetInstance().ScraperCache.ScrapeMovieURL(url)
} }
func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.StashBoxQueryInput) ([]*models.ScrapedScene, error) {
boxes := config.GetStashBoxes()
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex)
}
client := stashbox.NewClient(*boxes[input.StashBoxIndex])
if len(input.SceneIds) > 0 {
return client.FindStashBoxScenesByFingerprints(input.SceneIds)
}
if input.Q != nil {
return client.QueryStashBoxScene(*input.Q)
}
return nil, nil
}

View File

@@ -129,16 +129,12 @@ func Start() {
message := fmt.Sprintf("Internal system error. Error <%v>", err) message := fmt.Sprintf("Internal system error. Error <%v>", err)
return errors.New(message) return errors.New(message)
}) })
requestMiddleware := handler.RequestMiddleware(func(ctx context.Context, next func(ctx context.Context) []byte) []byte {
//api.GetRequestContext(ctx).Variables[]
return next(ctx)
})
websocketUpgrader := handler.WebsocketUpgrader(websocket.Upgrader{ websocketUpgrader := handler.WebsocketUpgrader(websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
return true return true
}, },
}) })
gqlHandler := handler.GraphQL(models.NewExecutableSchema(models.Config{Resolvers: &Resolver{}}), recoverFunc, requestMiddleware, websocketUpgrader) gqlHandler := handler.GraphQL(models.NewExecutableSchema(models.Config{Resolvers: &Resolver{}}), recoverFunc, websocketUpgrader)
r.Handle("/graphql", gqlHandler) r.Handle("/graphql", gqlHandler)
r.Handle("/playground", handler.Playground("GraphQL playground", "/graphql")) r.Handle("/playground", handler.Playground("GraphQL playground", "/graphql"))

87
pkg/models/scraped.go Normal file
View File

@@ -0,0 +1,87 @@
package models
import "strconv"
// MatchScrapedScenePerformer matches the provided performer with the
// performers in the database and sets the ID field if one is found.
func MatchScrapedScenePerformer(p *ScrapedScenePerformer) error {
qb := NewPerformerQueryBuilder()
performers, err := qb.FindByNames([]string{p.Name}, nil, true)
if err != nil {
return err
}
if len(performers) != 1 {
// ignore - cannot match
return nil
}
id := strconv.Itoa(performers[0].ID)
p.ID = &id
return nil
}
// MatchScrapedSceneStudio matches the provided studio with the studios
// in the database and sets the ID field if one is found.
func MatchScrapedSceneStudio(s *ScrapedSceneStudio) error {
qb := NewStudioQueryBuilder()
studio, err := qb.FindByName(s.Name, nil, true)
if err != nil {
return err
}
if studio == nil {
// ignore - cannot match
return nil
}
id := strconv.Itoa(studio.ID)
s.ID = &id
return nil
}
// MatchScrapedSceneMovie matches the provided movie with the movies
// in the database and sets the ID field if one is found.
func MatchScrapedSceneMovie(m *ScrapedSceneMovie) error {
qb := NewMovieQueryBuilder()
movies, err := qb.FindByNames([]string{m.Name}, nil, true)
if err != nil {
return err
}
if len(movies) != 1 {
// ignore - cannot match
return nil
}
id := strconv.Itoa(movies[0].ID)
m.ID = &id
return nil
}
// MatchScrapedSceneTag matches the provided tag with the tags
// in the database and sets the ID field if one is found.
func MatchScrapedSceneTag(s *ScrapedSceneTag) error {
qb := NewTagQueryBuilder()
tag, err := qb.FindByName(s.Name, nil, true)
if err != nil {
return err
}
if tag == nil {
// ignore - cannot match
return nil
}
id := strconv.Itoa(tag.ID)
s.ID = &id
return nil
}

View File

@@ -214,106 +214,30 @@ func (c Cache) ScrapePerformerURL(url string) (*models.ScrapedPerformer, error)
return nil, nil return nil, nil
} }
func matchPerformer(p *models.ScrapedScenePerformer) error {
qb := models.NewPerformerQueryBuilder()
performers, err := qb.FindByNames([]string{p.Name}, nil, true)
if err != nil {
return err
}
if len(performers) != 1 {
// ignore - cannot match
return nil
}
id := strconv.Itoa(performers[0].ID)
p.ID = &id
return nil
}
func matchStudio(s *models.ScrapedSceneStudio) error {
qb := models.NewStudioQueryBuilder()
studio, err := qb.FindByName(s.Name, nil, true)
if err != nil {
return err
}
if studio == nil {
// ignore - cannot match
return nil
}
id := strconv.Itoa(studio.ID)
s.ID = &id
return nil
}
func matchMovie(m *models.ScrapedSceneMovie) error {
qb := models.NewMovieQueryBuilder()
movies, err := qb.FindByNames([]string{m.Name}, nil, true)
if err != nil {
return err
}
if len(movies) != 1 {
// ignore - cannot match
return nil
}
id := strconv.Itoa(movies[0].ID)
m.ID = &id
return nil
}
func matchTag(s *models.ScrapedSceneTag) error {
qb := models.NewTagQueryBuilder()
tag, err := qb.FindByName(s.Name, nil, true)
if err != nil {
return err
}
if tag == nil {
// ignore - cannot match
return nil
}
id := strconv.Itoa(tag.ID)
s.ID = &id
return nil
}
func (c Cache) postScrapeScene(ret *models.ScrapedScene) error { func (c Cache) postScrapeScene(ret *models.ScrapedScene) error {
for _, p := range ret.Performers { for _, p := range ret.Performers {
err := matchPerformer(p) err := models.MatchScrapedScenePerformer(p)
if err != nil { if err != nil {
return err return err
} }
} }
for _, p := range ret.Movies { for _, p := range ret.Movies {
err := matchMovie(p) err := models.MatchScrapedSceneMovie(p)
if err != nil { if err != nil {
return err return err
} }
} }
for _, t := range ret.Tags { for _, t := range ret.Tags {
err := matchTag(t) err := models.MatchScrapedSceneTag(t)
if err != nil { if err != nil {
return err return err
} }
} }
if ret.Studio != nil { if ret.Studio != nil {
err := matchStudio(ret.Studio) err := models.MatchScrapedSceneStudio(ret.Studio)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -0,0 +1,559 @@
// Code generated by github.com/Yamashou/gqlgenc, DO NOT EDIT.
package graphql
import (
"context"
"net/http"
"github.com/Yamashou/gqlgenc/client"
)
type Client struct {
Client *client.Client
}
func NewClient(cli *http.Client, baseURL string, options ...client.HTTPRequestOption) *Client {
return &Client{Client: client.NewClient(cli, baseURL, options...)}
}
type Query struct {
FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\""
QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\""
FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\""
QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\""
FindTag *Tag "json:\"findTag\" graphql:\"findTag\""
QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\""
FindScene *Scene "json:\"findScene\" graphql:\"findScene\""
FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\""
FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\""
QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\""
FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\""
QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\""
FindUser *User "json:\"findUser\" graphql:\"findUser\""
QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\""
Me *User "json:\"me\" graphql:\"me\""
SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\""
SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\""
Version Version "json:\"version\" graphql:\"version\""
}
type Mutation struct {
SceneCreate *Scene "json:\"sceneCreate\" graphql:\"sceneCreate\""
SceneUpdate *Scene "json:\"sceneUpdate\" graphql:\"sceneUpdate\""
SceneDestroy bool "json:\"sceneDestroy\" graphql:\"sceneDestroy\""
PerformerCreate *Performer "json:\"performerCreate\" graphql:\"performerCreate\""
PerformerUpdate *Performer "json:\"performerUpdate\" graphql:\"performerUpdate\""
PerformerDestroy bool "json:\"performerDestroy\" graphql:\"performerDestroy\""
StudioCreate *Studio "json:\"studioCreate\" graphql:\"studioCreate\""
StudioUpdate *Studio "json:\"studioUpdate\" graphql:\"studioUpdate\""
StudioDestroy bool "json:\"studioDestroy\" graphql:\"studioDestroy\""
TagCreate *Tag "json:\"tagCreate\" graphql:\"tagCreate\""
TagUpdate *Tag "json:\"tagUpdate\" graphql:\"tagUpdate\""
TagDestroy bool "json:\"tagDestroy\" graphql:\"tagDestroy\""
UserCreate *User "json:\"userCreate\" graphql:\"userCreate\""
UserUpdate *User "json:\"userUpdate\" graphql:\"userUpdate\""
UserDestroy bool "json:\"userDestroy\" graphql:\"userDestroy\""
ImageCreate *Image "json:\"imageCreate\" graphql:\"imageCreate\""
ImageUpdate *Image "json:\"imageUpdate\" graphql:\"imageUpdate\""
ImageDestroy bool "json:\"imageDestroy\" graphql:\"imageDestroy\""
RegenerateAPIKey string "json:\"regenerateAPIKey\" graphql:\"regenerateAPIKey\""
ChangePassword bool "json:\"changePassword\" graphql:\"changePassword\""
SceneEdit Edit "json:\"sceneEdit\" graphql:\"sceneEdit\""
PerformerEdit Edit "json:\"performerEdit\" graphql:\"performerEdit\""
StudioEdit Edit "json:\"studioEdit\" graphql:\"studioEdit\""
TagEdit Edit "json:\"tagEdit\" graphql:\"tagEdit\""
EditVote Edit "json:\"editVote\" graphql:\"editVote\""
EditComment Edit "json:\"editComment\" graphql:\"editComment\""
ApplyEdit Edit "json:\"applyEdit\" graphql:\"applyEdit\""
CancelEdit Edit "json:\"cancelEdit\" graphql:\"cancelEdit\""
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
}
type URLFragment struct {
URL string "json:\"url\" graphql:\"url\""
Type string "json:\"type\" graphql:\"type\""
}
type ImageFragment struct {
ID string "json:\"id\" graphql:\"id\""
URL string "json:\"url\" graphql:\"url\""
Width *int "json:\"width\" graphql:\"width\""
Height *int "json:\"height\" graphql:\"height\""
}
type StudioFragment struct {
Name string "json:\"name\" graphql:\"name\""
ID string "json:\"id\" graphql:\"id\""
Urls []*URLFragment "json:\"urls\" graphql:\"urls\""
Images []*ImageFragment "json:\"images\" graphql:\"images\""
}
type TagFragment struct {
Name string "json:\"name\" graphql:\"name\""
ID string "json:\"id\" graphql:\"id\""
}
type FuzzyDateFragment struct {
Date string "json:\"date\" graphql:\"date\""
Accuracy DateAccuracyEnum "json:\"accuracy\" graphql:\"accuracy\""
}
type MeasurementsFragment struct {
BandSize *int "json:\"band_size\" graphql:\"band_size\""
CupSize *string "json:\"cup_size\" graphql:\"cup_size\""
Waist *int "json:\"waist\" graphql:\"waist\""
Hip *int "json:\"hip\" graphql:\"hip\""
}
type BodyModificationFragment struct {
Location string "json:\"location\" graphql:\"location\""
Description *string "json:\"description\" graphql:\"description\""
}
type PerformerFragment struct {
ID string "json:\"id\" graphql:\"id\""
Name string "json:\"name\" graphql:\"name\""
Disambiguation *string "json:\"disambiguation\" graphql:\"disambiguation\""
Aliases []string "json:\"aliases\" graphql:\"aliases\""
Gender *GenderEnum "json:\"gender\" graphql:\"gender\""
Urls []*URLFragment "json:\"urls\" graphql:\"urls\""
Images []*ImageFragment "json:\"images\" graphql:\"images\""
Birthdate *FuzzyDateFragment "json:\"birthdate\" graphql:\"birthdate\""
Ethnicity *EthnicityEnum "json:\"ethnicity\" graphql:\"ethnicity\""
Country *string "json:\"country\" graphql:\"country\""
EyeColor *EyeColorEnum "json:\"eye_color\" graphql:\"eye_color\""
HairColor *HairColorEnum "json:\"hair_color\" graphql:\"hair_color\""
Height *int "json:\"height\" graphql:\"height\""
Measurements MeasurementsFragment "json:\"measurements\" graphql:\"measurements\""
BreastType *BreastTypeEnum "json:\"breast_type\" graphql:\"breast_type\""
CareerStartYear *int "json:\"career_start_year\" graphql:\"career_start_year\""
CareerEndYear *int "json:\"career_end_year\" graphql:\"career_end_year\""
Tattoos []*BodyModificationFragment "json:\"tattoos\" graphql:\"tattoos\""
Piercings []*BodyModificationFragment "json:\"piercings\" graphql:\"piercings\""
}
type PerformerAppearanceFragment struct {
As *string "json:\"as\" graphql:\"as\""
Performer PerformerFragment "json:\"performer\" graphql:\"performer\""
}
type FingerprintFragment struct {
Algorithm FingerprintAlgorithm "json:\"algorithm\" graphql:\"algorithm\""
Hash string "json:\"hash\" graphql:\"hash\""
Duration int "json:\"duration\" graphql:\"duration\""
}
type SceneFragment struct {
ID string "json:\"id\" graphql:\"id\""
Title *string "json:\"title\" graphql:\"title\""
Details *string "json:\"details\" graphql:\"details\""
Duration *int "json:\"duration\" graphql:\"duration\""
Date *string "json:\"date\" graphql:\"date\""
Urls []*URLFragment "json:\"urls\" graphql:\"urls\""
Images []*ImageFragment "json:\"images\" graphql:\"images\""
Studio *StudioFragment "json:\"studio\" graphql:\"studio\""
Tags []*TagFragment "json:\"tags\" graphql:\"tags\""
Performers []*PerformerAppearanceFragment "json:\"performers\" graphql:\"performers\""
Fingerprints []*FingerprintFragment "json:\"fingerprints\" graphql:\"fingerprints\""
}
type FindSceneByFingerprint struct {
FindSceneByFingerprint []*SceneFragment "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\""
}
type FindScenesByFingerprints struct {
FindScenesByFingerprints []*SceneFragment "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\""
}
type SearchScene struct {
SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\""
}
type SubmitFingerprintPayload struct {
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
}
const FindSceneByFingerprintQuery = `query FindSceneByFingerprint ($fingerprint: FingerprintQueryInput!) {
findSceneByFingerprint(fingerprint: $fingerprint) {
... SceneFragment
}
}
fragment SceneFragment on Scene {
id
title
details
duration
date
urls {
... URLFragment
}
images {
... ImageFragment
}
studio {
... StudioFragment
}
tags {
... TagFragment
}
performers {
... PerformerAppearanceFragment
}
fingerprints {
... FingerprintFragment
}
}
fragment TagFragment on Tag {
name
id
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
urls {
... URLFragment
}
images {
... ImageFragment
}
birthdate {
... FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
... MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
... BodyModificationFragment
}
piercings {
... BodyModificationFragment
}
}
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
... URLFragment
}
images {
... ImageFragment
}
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
`
func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) {
vars := map[string]interface{}{
"fingerprint": fingerprint,
}
var res FindSceneByFingerprint
if err := c.Client.Post(ctx, FindSceneByFingerprintQuery, &res, vars, httpRequestOptions...); err != nil {
return nil, err
}
return &res, nil
}
const FindScenesByFingerprintsQuery = `query FindScenesByFingerprints ($fingerprints: [String!]!) {
findScenesByFingerprints(fingerprints: $fingerprints) {
... SceneFragment
}
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
... URLFragment
}
images {
... ImageFragment
}
}
fragment TagFragment on Tag {
name
id
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment SceneFragment on Scene {
id
title
details
duration
date
urls {
... URLFragment
}
images {
... ImageFragment
}
studio {
... StudioFragment
}
tags {
... TagFragment
}
performers {
... PerformerAppearanceFragment
}
fingerprints {
... FingerprintFragment
}
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
urls {
... URLFragment
}
images {
... ImageFragment
}
birthdate {
... FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
... MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
... BodyModificationFragment
}
piercings {
... BodyModificationFragment
}
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment URLFragment on URL {
url
type
}
`
func (c *Client) FindScenesByFingerprints(ctx context.Context, fingerprints []string, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFingerprints, error) {
vars := map[string]interface{}{
"fingerprints": fingerprints,
}
var res FindScenesByFingerprints
if err := c.Client.Post(ctx, FindScenesByFingerprintsQuery, &res, vars, httpRequestOptions...); err != nil {
return nil, err
}
return &res, nil
}
const SearchSceneQuery = `query SearchScene ($term: String!) {
searchScene(term: $term) {
... SceneFragment
}
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment SceneFragment on Scene {
id
title
details
duration
date
urls {
... URLFragment
}
images {
... ImageFragment
}
studio {
... StudioFragment
}
tags {
... TagFragment
}
performers {
... PerformerAppearanceFragment
}
fingerprints {
... FingerprintFragment
}
}
fragment TagFragment on Tag {
name
id
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
urls {
... URLFragment
}
images {
... ImageFragment
}
birthdate {
... FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
... MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
... BodyModificationFragment
}
piercings {
... BodyModificationFragment
}
}
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
... URLFragment
}
images {
... ImageFragment
}
}
fragment BodyModificationFragment on BodyModification {
location
description
}
`
func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) {
vars := map[string]interface{}{
"term": term,
}
var res SearchScene
if err := c.Client.Post(ctx, SearchSceneQuery, &res, vars, httpRequestOptions...); err != nil {
return nil, err
}
return &res, nil
}
const SubmitFingerprintQuery = `mutation SubmitFingerprint ($input: FingerprintSubmission!) {
submitFingerprint(input: $input)
}
`
func (c *Client) SubmitFingerprint(ctx context.Context, input FingerprintSubmission, httpRequestOptions ...client.HTTPRequestOption) (*SubmitFingerprintPayload, error) {
vars := map[string]interface{}{
"input": input,
}
var res SubmitFingerprintPayload
if err := c.Client.Post(ctx, SubmitFingerprintQuery, &res, vars, httpRequestOptions...); err != nil {
return nil, err
}
return &res, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,315 @@
package stashbox
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
"github.com/Yamashou/gqlgenc/client"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scraper/stashbox/graphql"
"github.com/stashapp/stash/pkg/utils"
)
// Timeout to get the image. Includes transfer time. May want to make this
// configurable at some point.
const imageGetTimeout = time.Second * 30
// Client represents the client interface to a stash-box server instance.
type Client struct {
client *graphql.Client
}
// NewClient returns a new instance of a stash-box client.
func NewClient(box models.StashBox) *Client {
authHeader := func(req *http.Request) {
req.Header.Set("ApiKey", box.APIKey)
}
client := &graphql.Client{
Client: client.NewClient(http.DefaultClient, box.Endpoint, authHeader),
}
return &Client{
client: client,
}
}
// QueryStashBoxScene queries stash-box for scenes using a query string.
func (c Client) QueryStashBoxScene(queryStr string) ([]*models.ScrapedScene, error) {
scenes, err := c.client.SearchScene(context.TODO(), queryStr)
if err != nil {
return nil, err
}
sceneFragments := scenes.SearchScene
var ret []*models.ScrapedScene
for _, s := range sceneFragments {
ss, err := sceneFragmentToScrapedScene(s)
if err != nil {
return nil, err
}
ret = append(ret, ss)
}
return ret, nil
}
// FindStashBoxScenesByFingerprints queries stash-box for scenes using every
// scene's MD5 checksum and/or oshash.
func (c Client) FindStashBoxScenesByFingerprints(sceneIDs []string) ([]*models.ScrapedScene, error) {
qb := models.NewSceneQueryBuilder()
var fingerprints []string
for _, sceneID := range sceneIDs {
idInt, _ := strconv.Atoi(sceneID)
scene, err := qb.Find(idInt)
if err != nil {
return nil, err
}
if scene == nil {
return nil, fmt.Errorf("scene with id %d not found", idInt)
}
if scene.Checksum.Valid {
fingerprints = append(fingerprints, scene.Checksum.String)
}
if scene.OSHash.Valid {
fingerprints = append(fingerprints, scene.OSHash.String)
}
}
return c.findStashBoxScenesByFingerprints(fingerprints)
}
func (c Client) findStashBoxScenesByFingerprints(fingerprints []string) ([]*models.ScrapedScene, error) {
scenes, err := c.client.FindScenesByFingerprints(context.TODO(), fingerprints)
if err != nil {
return nil, err
}
sceneFragments := scenes.FindScenesByFingerprints
var ret []*models.ScrapedScene
for _, s := range sceneFragments {
ss, err := sceneFragmentToScrapedScene(s)
if err != nil {
return nil, err
}
ret = append(ret, ss)
}
return ret, nil
}
func findURL(urls []*graphql.URLFragment, urlType string) *string {
for _, u := range urls {
if u.Type == urlType {
ret := u.URL
return &ret
}
}
return nil
}
func enumToStringPtr(e fmt.Stringer) *string {
if e != nil {
ret := e.String()
return &ret
}
return nil
}
func formatMeasurements(m graphql.MeasurementsFragment) *string {
if m.BandSize != nil && m.CupSize != nil && m.Hip != nil && m.Waist != nil {
ret := fmt.Sprintf("%d%s-%d-%d", *m.BandSize, *m.CupSize, *m.Waist, *m.Hip)
return &ret
}
return nil
}
func formatCareerLength(start, end *int) *string {
if start == nil && end == nil {
return nil
}
var ret string
if end == nil {
ret = fmt.Sprintf("%d -", *start)
} else {
ret = fmt.Sprintf("%d - %d", *start, *end)
}
return &ret
}
func formatBodyModifications(m []*graphql.BodyModificationFragment) *string {
if len(m) == 0 {
return nil
}
var retSlice []string
for _, f := range m {
if f.Description == nil {
retSlice = append(retSlice, f.Location)
} else {
retSlice = append(retSlice, fmt.Sprintf("%s, %s", f.Location, *f.Description))
}
}
ret := strings.Join(retSlice, "; ")
return &ret
}
func fetchImage(url string) (*string, error) {
client := &http.Client{
Timeout: imageGetTimeout,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// determine the image type and set the base64 type
contentType := resp.Header.Get("Content-Type")
if contentType == "" {
contentType = http.DetectContentType(body)
}
img := "data:" + contentType + ";base64," + utils.GetBase64StringFromData(body)
return &img, nil
}
func performerFragmentToScrapedScenePerformer(p graphql.PerformerFragment) *models.ScrapedScenePerformer {
sp := &models.ScrapedScenePerformer{
Name: p.Name,
Country: p.Country,
Measurements: formatMeasurements(p.Measurements),
CareerLength: formatCareerLength(p.CareerStartYear, p.CareerEndYear),
Tattoos: formatBodyModifications(p.Tattoos),
Piercings: formatBodyModifications(p.Piercings),
Twitter: findURL(p.Urls, "TWITTER"),
// TODO - Image - should be returned as a set of URLs. Will need a
// graphql schema change to accommodate this. Leave off for now.
}
if p.Height != nil {
hs := strconv.Itoa(*p.Height)
sp.Height = &hs
}
if p.Birthdate != nil {
b := p.Birthdate.Date
sp.Birthdate = &b
}
if p.Gender != nil {
sp.Gender = enumToStringPtr(p.Gender)
}
if p.Ethnicity != nil {
sp.Ethnicity = enumToStringPtr(p.Ethnicity)
}
if p.EyeColor != nil {
sp.EyeColor = enumToStringPtr(p.EyeColor)
}
if p.BreastType != nil {
sp.FakeTits = enumToStringPtr(p.BreastType)
}
return sp
}
func getFirstImage(images []*graphql.ImageFragment) *string {
ret, err := fetchImage(images[0].URL)
if err != nil {
logger.Warnf("Error fetching image %s: %s", images[0].URL, err.Error())
}
return ret
}
func sceneFragmentToScrapedScene(s *graphql.SceneFragment) (*models.ScrapedScene, error) {
ss := &models.ScrapedScene{
Title: s.Title,
Date: s.Date,
Details: s.Details,
URL: findURL(s.Urls, "STUDIO"),
// Image
// stash_id
}
if len(s.Images) > 0 {
// TODO - #454 code sorts images by aspect ratio according to a wanted
// orientation. I'm just grabbing the first for now
ss.Image = getFirstImage(s.Images)
}
if s.Studio != nil {
ss.Studio = &models.ScrapedSceneStudio{
Name: s.Studio.Name,
URL: findURL(s.Studio.Urls, "HOME"),
}
err := models.MatchScrapedSceneStudio(ss.Studio)
if err != nil {
return nil, err
}
}
for _, p := range s.Performers {
sp := performerFragmentToScrapedScenePerformer(p.Performer)
err := models.MatchScrapedScenePerformer(sp)
if err != nil {
return nil, err
}
ss.Performers = append(ss.Performers, sp)
}
for _, t := range s.Tags {
st := &models.ScrapedSceneTag{
Name: t.Name,
}
err := models.MatchScrapedSceneTag(st)
if err != nil {
return nil, err
}
ss.Tags = append(ss.Tags, st)
}
return ss, nil
}

View File

@@ -4,5 +4,6 @@ package main
import ( import (
_ "github.com/99designs/gqlgen" _ "github.com/99designs/gqlgen"
_ "github.com/Yamashou/gqlgenc"
_ "github.com/vektra/mockery/v2" _ "github.com/vektra/mockery/v2"
) )

View File

@@ -14,6 +14,8 @@ import {
useListSceneScrapers, useListSceneScrapers,
useSceneUpdate, useSceneUpdate,
mutateReloadScrapers, mutateReloadScrapers,
useConfiguration,
queryStashBoxScene,
} from "src/core/StashService"; } from "src/core/StashService";
import { import {
PerformerSelect, PerformerSelect,
@@ -62,6 +64,8 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
const [coverImagePreview, setCoverImagePreview] = useState<string>(); const [coverImagePreview, setCoverImagePreview] = useState<string>();
const stashConfig = useConfiguration();
// Network state // Network state
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@@ -117,7 +121,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
); );
setQueryableScrapers(newQueryableScrapers); setQueryableScrapers(newQueryableScrapers);
}, [Scrapers]); }, [Scrapers, stashConfig]);
useEffect(() => { useEffect(() => {
let changed = false; let changed = false;
@@ -251,11 +255,40 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
ImageUtils.onImageChange(event, onImageLoad); ImageUtils.onImageChange(event, onImageLoad);
} }
async function onScrapeStashBoxClicked(stashBoxIndex: number) {
setIsLoading(true);
try {
const result = await queryStashBoxScene(stashBoxIndex, props.scene.id);
if (!result.data || !result.data.queryStashBoxScene) {
return;
}
if (result.data.queryStashBoxScene.length > 0) {
setScrapedScene(result.data.queryStashBoxScene[0]);
} else {
Toast.success({
content: "No scenes found",
});
}
} catch (e) {
Toast.error(e);
} finally {
setIsLoading(false);
}
}
// function onStashBoxQueryClicked(/* stashBoxIndex: number */) {
// TODO
// }
async function onScrapeClicked(scraper: GQL.Scraper) { async function onScrapeClicked(scraper: GQL.Scraper) {
setIsLoading(true); setIsLoading(true);
try { try {
const result = await queryScrapeScene(scraper.id, getSceneInput()); const result = await queryScrapeScene(scraper.id, getSceneInput());
if (!result.data || !result.data.scrapeScene) { if (!result.data || !result.data.scrapeScene) {
Toast.success({
content: "No scenes found",
});
return; return;
} }
setScrapedScene(result.data.scrapeScene); setScrapedScene(result.data.scrapeScene);
@@ -309,8 +342,23 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
} }
function renderScraperMenu() { function renderScraperMenu() {
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
// TODO - change name based on stashbox configuration
return ( return (
<DropdownButton id="scene-scrape" title="Scrape with..."> <DropdownButton
className="d-inline-block"
id="scene-scrape"
title="Scrape with..."
>
{stashBoxes.map((s, index) => (
<Dropdown.Item
key={s.endpoint}
onClick={() => onScrapeStashBoxClicked(index)}
>
stash-box
</Dropdown.Item>
))}
{queryableScrapers.map((s) => ( {queryableScrapers.map((s) => (
<Dropdown.Item key={s.name} onClick={() => onScrapeClicked(s)}> <Dropdown.Item key={s.name} onClick={() => onScrapeClicked(s)}>
{s.name} {s.name}
@@ -326,6 +374,44 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
); );
} }
function maybeRenderStashboxQueryButton() {
// const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
// if (stashBoxes.length === 0) {
// return;
// }
// TODO - hide this button for now, with the view to add it when we get
// the query dialog going
// if (stashBoxes.length === 1) {
// return (
// <Button
// className="mr-1"
// onClick={() => onStashBoxQueryClicked(0)}
// title="Query"
// >
// <Icon className="fa-fw" icon="search" />
// </Button>
// );
// }
// // TODO - change name based on stashbox configuration
// return (
// <Dropdown className="d-inline-block mr-1">
// <Dropdown.Toggle id="stashbox-query-dropdown">
// <Icon className="fa-fw" icon="search" />
// </Dropdown.Toggle>
// <Dropdown.Menu>
// {stashBoxes.map((s, index) => (
// <Dropdown.Item
// key={s.endpoint}
// onClick={() => onStashBoxQueryClicked(index)}
// >
// stash-box
// </Dropdown.Item>
// ))}
// </Dropdown.Menu>
// </Dropdown>
// );
}
function urlScrapable(scrapedUrl: string): boolean { function urlScrapable(scrapedUrl: string): boolean {
return (Scrapers?.data?.listSceneScrapers ?? []).some((s) => return (Scrapers?.data?.listSceneScrapers ?? []).some((s) =>
(s?.scene?.urls ?? []).some((u) => scrapedUrl.includes(u)) (s?.scene?.urls ?? []).some((u) => scrapedUrl.includes(u))
@@ -432,7 +518,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
<div id="scene-edit-details"> <div id="scene-edit-details">
{maybeRenderScrapeDialog()} {maybeRenderScrapeDialog()}
<div className="form-container row px-3 pt-3"> <div className="form-container row px-3 pt-3">
<div className="col edit-buttons mb-3 pl-0"> <div className="col-6 edit-buttons mb-3 pl-0">
<Button className="edit-button" variant="primary" onClick={onSave}> <Button className="edit-button" variant="primary" onClick={onSave}>
Save Save
</Button> </Button>
@@ -444,7 +530,10 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
Delete Delete
</Button> </Button>
</div> </div>
{renderScraperMenu()} <Col xs={6} className="text-right">
{maybeRenderStashboxQueryButton()}
{renderScraperMenu()}
</Col>
</div> </div>
<div className="form-container row px-3"> <div className="form-container row px-3">
<div className="col-12 col-lg-6 col-xl-12"> <div className="col-12 col-lg-6 col-xl-12">

View File

@@ -498,6 +498,17 @@ export const queryScrapeScene = (
fetchPolicy: "network-only", fetchPolicy: "network-only",
}); });
export const queryStashBoxScene = (stashBoxIndex: number, sceneID: string) =>
client.query<GQL.QueryStashBoxSceneQuery>({
query: GQL.QueryStashBoxSceneDocument,
variables: {
input: {
stash_box_index: stashBoxIndex,
scene_ids: [sceneID],
},
},
});
export const mutateReloadScrapers = () => export const mutateReloadScrapers = () =>
client.mutate<GQL.ReloadScrapersMutation>({ client.mutate<GQL.ReloadScrapersMutation>({
mutation: GQL.ReloadScrapersDocument, mutation: GQL.ReloadScrapersDocument,

View File

@@ -4,6 +4,8 @@
/integration/node_modules /integration/node_modules
/integration/schema-fetched.graphql /integration/schema-fetched.graphql
/example/chat/package-lock.json /example/chat/package-lock.json
/example/federation/package-lock.json
/example/federation/node_modules
/codegen/gen /codegen/gen
/gen /gen

View File

@@ -1,3 +1,39 @@
run:
tests: true
skip-dirs:
- bin
linters-settings: linters-settings:
errcheck: errcheck:
ignore: fmt:.*,[rR]ead|[wW]rite|[cC]lose,io:Copy ignore: fmt:.*,[rR]ead|[wW]rite|[cC]lose,io:Copy
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dupl
- errcheck
- gocritic
- gofmt
- goimports
- gosimple
- govet
- ineffassign
- interfacer
- misspell
- nakedret
- prealloc
- staticcheck
- structcheck
- unconvert
- unused
- varcheck
issues:
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- dupl

View File

@@ -1,4 +1,4 @@
Copyright (c) 2018 Adam Scarr Copyright (c) 2020 gqlgen authors
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,20 +1,24 @@
# gqlgen [![CircleCI](https://badgen.net/circleci/github/99designs/gqlgen/master)](https://circleci.com/gh/99designs/gqlgen) [![Read the Docs](https://badgen.net/badge/docs/available/green)](http://gqlgen.com/) # gqlgen [![Continuous Integration](https://github.com/99designs/gqlgen/workflows/Continuous%20Integration/badge.svg)](https://github.com/99designs/gqlgen/actions) [![Read the Docs](https://badgen.net/badge/docs/available/green)](http://gqlgen.com/) [![GoDoc](https://godoc.org/github.com/99designs/gqlgen?status.svg)](https://godoc.org/github.com/99designs/gqlgen)
![gqlgen](https://user-images.githubusercontent.com/46195831/89802919-0bb8ef00-db2a-11ea-8ba4-88e7a58b2fd2.png)
## What is gqlgen? ## What is gqlgen?
[gqlgen](https://github.com/99designs/gqlgen) is a Go library for building GraphQL servers without any fuss. gqlgen is: [gqlgen](https://github.com/99designs/gqlgen) is a Go library for building GraphQL servers without any fuss.<br/>
- **Schema first** — Define your API using the GraphQL [Schema Definition Language](http://graphql.org/learn/schema/). - **gqlgen is based on a Schema first approach** — You get to Define your API using the GraphQL [Schema Definition Language](http://graphql.org/learn/schema/).
- **Type safe** — You should never see `map[string]interface{}` here. - **gqlgen priortizes Type safety** — You should never see `map[string]interface{}` here.
- **Codegen** — Let us generate the boring bits, so you can build your app quickly. - **gqlgen enables Codegen** — We generate the boring bits, so you can focus on building your app quickly.
[Feature Comparison](https://gqlgen.com/feature-comparison/) Still not convinced enough to use **gqlgen**? Compare **gqlgen** with other Go graphql [implementations](https://gqlgen.com/feature-comparison/)
## Getting Started ## Getting Started
- To install gqlgen run the comand `go get github.com/99designs/gqlgen` in your project directory.<br/>
- You could initialize a new project using the recommended folder structure by running this command `go run github.com/99designs/gqlgen init`.
First work your way through the [Getting Started](https://gqlgen.com/getting-started/) tutorial. You could find a more comprehensive guide to help you get started [here](https://gqlgen.com/getting-started/).<br/>
We also have a couple of real-world [examples](https://github.com/99designs/gqlgen/tree/master/example) that show how to GraphQL applicatons with **gqlgen** seamlessly,
If you can't find what your looking for, look at our [examples](https://github.com/99designs/gqlgen/tree/master/example) for example usage of gqlgen. You can see these [examples](https://github.com/99designs/gqlgen/tree/master/example) here or visit [godoc](https://godoc.org/github.com/99designs/gqlgen).
## Reporting Issues ## Reporting Issues
@@ -22,10 +26,88 @@ If you think you've found a bug, or something isn't behaving the way you think i
## Contributing ## Contributing
Read our [Contribution Guidelines](https://github.com/99designs/gqlgen/blob/master/CONTRIBUTING.md) for information on how you can help out gqlgen. We welcome contributions, Read our [Contribution Guidelines](https://github.com/99designs/gqlgen/blob/master/CONTRIBUTING.md) to learn more about contributing to **gqlgen**
## Frequently asked questions
### How do I prevent fetching child objects that might not be used?
When you have nested or recursive schema like this:
```graphql
type User {
id: ID!
name: String!
friends: [User!]!
}
```
You need to tell gqlgen that it should only fetch friends if the user requested it. There are two ways to do this;
- #### Using Custom Models
Write a custom model that omits the friends field:
```go
type User struct {
ID int
Name string
}
```
And reference the model in `gqlgen.yml`:
```yaml
# gqlgen.yml
models:
User:
model: github.com/you/pkg/model.User # go import path to the User struct above
```
- #### Using Explicit Resolvers
If you want to Keep using the generated model, mark the field as requiring a resolver explicitly in `gqlgen.yml` like this:
```yaml
# gqlgen.yml
models:
User:
fields:
friends:
resolver: true # force a resolver to be generated
```
After doing either of the above and running generate we will need to provide a resolver for friends:
```go
func (r *userResolver) Friends(ctx context.Context, obj *User) ([]*User, error) {
// select * from user where friendid = obj.ID
return friends, nil
}
```
### Can I change the type of the ID from type String to Type Int?
Yes! You can by remapping it in config as seen below:
```yaml
models:
ID: # The GraphQL type ID is backed by
model:
- github.com/99designs/gqlgen/graphql.IntID # An go integer
- github.com/99designs/gqlgen/graphql.ID # or a go string
```
This means gqlgen will be able to automatically bind to strings or ints for models you have written yourself, but the
first model in this list is used as the default type and it will always be used when:
- Generating models based on schema
- As arguments in resolvers
There isnt any way around this, gqlgen has no way to know what you want in a given context.
## Other Resources ## Other Resources
- [Christopher Biscardi @ Gophercon UK 2018](https://youtu.be/FdURVezcdcw) - [Christopher Biscardi @ Gophercon UK 2018](https://youtu.be/FdURVezcdcw)
- [Introducing gqlgen: a GraphQL Server Generator for Go](https://99designs.com.au/blog/engineering/gqlgen-a-graphql-server-generator-for-go/) - [Introducing gqlgen: a GraphQL Server Generator for Go](https://99designs.com.au/blog/engineering/gqlgen-a-graphql-server-generator-for-go/)
- [GraphQL workshop for Golang developers by Iván Corrales Solera](https://graphql-go.wesovilabs.com) - [Dive into GraphQL by Iván Corrales Solera](https://medium.com/@ivan.corrales.solera/dive-into-graphql-9bfedf22e1a)
- [Sample Project built on gqlgen with Postgres by Oleg Shalygin](https://github.com/oshalygin/gqlgen-pg-todo-example)

View File

@@ -6,25 +6,60 @@ import (
"github.com/99designs/gqlgen/codegen" "github.com/99designs/gqlgen/codegen"
"github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/plugin" "github.com/99designs/gqlgen/plugin"
"github.com/99designs/gqlgen/plugin/federation"
"github.com/99designs/gqlgen/plugin/modelgen" "github.com/99designs/gqlgen/plugin/modelgen"
"github.com/99designs/gqlgen/plugin/resolvergen" "github.com/99designs/gqlgen/plugin/resolvergen"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/tools/go/packages"
) )
func Generate(cfg *config.Config, option ...Option) error { func Generate(cfg *config.Config, option ...Option) error {
_ = syscall.Unlink(cfg.Exec.Filename) _ = syscall.Unlink(cfg.Exec.Filename)
_ = syscall.Unlink(cfg.Model.Filename) if cfg.Model.IsDefined() {
_ = syscall.Unlink(cfg.Model.Filename)
}
plugins := []plugin.Plugin{ plugins := []plugin.Plugin{}
modelgen.New(), if cfg.Model.IsDefined() {
resolvergen.New(), plugins = append(plugins, modelgen.New())
}
plugins = append(plugins, resolvergen.New())
if cfg.Federation.IsDefined() {
plugins = append([]plugin.Plugin{federation.New()}, plugins...)
} }
for _, o := range option { for _, o := range option {
o(cfg, &plugins) o(cfg, &plugins)
} }
for _, p := range plugins {
if inj, ok := p.(plugin.EarlySourceInjector); ok {
if s := inj.InjectSourceEarly(); s != nil {
cfg.Sources = append(cfg.Sources, s)
}
}
}
if err := cfg.LoadSchema(); err != nil {
return errors.Wrap(err, "failed to load schema")
}
for _, p := range plugins {
if inj, ok := p.(plugin.LateSourceInjector); ok {
if s := inj.InjectSourceLate(cfg.Schema); s != nil {
cfg.Sources = append(cfg.Sources, s)
}
}
}
// LoadSchema again now we have everything
if err := cfg.LoadSchema(); err != nil {
return errors.Wrap(err, "failed to load schema")
}
if err := cfg.Init(); err != nil {
return errors.Wrap(err, "generating core failed")
}
for _, p := range plugins { for _, p := range plugins {
if mut, ok := p.(plugin.ConfigMutator); ok { if mut, ok := p.(plugin.ConfigMutator); ok {
err := mut.MutateConfig(cfg) err := mut.MutateConfig(cfg)
@@ -36,7 +71,7 @@ func Generate(cfg *config.Config, option ...Option) error {
// Merge again now that the generated models have been injected into the typemap // Merge again now that the generated models have been injected into the typemap
data, err := codegen.BuildData(cfg) data, err := codegen.BuildData(cfg)
if err != nil { if err != nil {
return errors.Wrap(err, "merging failed") return errors.Wrap(err, "merging type systems failed")
} }
if err = codegen.GenerateCode(data); err != nil { if err = codegen.GenerateCode(data); err != nil {
@@ -52,8 +87,14 @@ func Generate(cfg *config.Config, option ...Option) error {
} }
} }
if err := validate(cfg); err != nil { if err = codegen.GenerateCode(data); err != nil {
return errors.Wrap(err, "validation failed") return errors.Wrap(err, "generating core failed")
}
if !cfg.SkipValidation {
if err := validate(cfg); err != nil {
return errors.Wrap(err, "validation failed")
}
} }
return nil return nil
@@ -68,9 +109,11 @@ func validate(cfg *config.Config) error {
if cfg.Resolver.IsDefined() { if cfg.Resolver.IsDefined() {
roots = append(roots, cfg.Resolver.ImportPath()) roots = append(roots, cfg.Resolver.ImportPath())
} }
_, err := packages.Load(&packages.Config{Mode: packages.LoadTypes | packages.LoadSyntax}, roots...)
if err != nil { cfg.Packages.LoadAll(roots...)
return errors.Wrap(err, "validation failed") errs := cfg.Packages.Errors()
if len(errs) > 0 {
return errs
} }
return nil return nil
} }

View File

@@ -1,32 +0,0 @@
version: "{build}"
# Source Config
skip_branch_with_pr: true
clone_folder: c:\projects\gqlgen
# Build host
environment:
GOPATH: c:\gopath
GOVERSION: 1.11.5
PATH: '%PATH%;c:\gopath\bin'
init:
- git config --global core.autocrlf input
# Build
install:
# Install the specific Go version.
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.msi
- msiexec /i go%GOVERSION%.windows-amd64.msi /q
- go version
build: false
deploy: false
test_script:
- go generate ./...
- go test -timeout 20m ./...

View File

@@ -5,6 +5,6 @@ import (
// don't prune unused code for us. Both lists should be kept in sync. // don't prune unused code for us. Both lists should be kept in sync.
_ "github.com/99designs/gqlgen/graphql" _ "github.com/99designs/gqlgen/graphql"
_ "github.com/99designs/gqlgen/graphql/introspection" _ "github.com/99designs/gqlgen/graphql/introspection"
_ "github.com/vektah/gqlparser" _ "github.com/vektah/gqlparser/v2"
_ "github.com/vektah/gqlparser/ast" _ "github.com/vektah/gqlparser/v2/ast"
) )

View File

@@ -1,44 +1,43 @@
package cmd package cmd
import ( import (
"fmt"
"os" "os"
"github.com/99designs/gqlgen/api" "github.com/99designs/gqlgen/api"
"github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/config"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli" "github.com/urfave/cli/v2"
) )
var genCmd = cli.Command{ var genCmd = &cli.Command{
Name: "generate", Name: "generate",
Usage: "generate a graphql server based on schema", Usage: "generate a graphql server based on schema",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.BoolFlag{Name: "verbose, v", Usage: "show logs"}, &cli.BoolFlag{Name: "verbose, v", Usage: "show logs"},
cli.StringFlag{Name: "config, c", Usage: "the config filename"}, &cli.StringFlag{Name: "config, c", Usage: "the config filename"},
}, },
Action: func(ctx *cli.Context) { Action: func(ctx *cli.Context) error {
var cfg *config.Config var cfg *config.Config
var err error var err error
if configFilename := ctx.String("config"); configFilename != "" { if configFilename := ctx.String("config"); configFilename != "" {
cfg, err = config.LoadConfig(configFilename) cfg, err = config.LoadConfig(configFilename)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err.Error()) return err
os.Exit(1)
} }
} else { } else {
cfg, err = config.LoadConfigFromDefaultLocations() cfg, err = config.LoadConfigFromDefaultLocations()
if os.IsNotExist(errors.Cause(err)) { if os.IsNotExist(errors.Cause(err)) {
cfg = config.DefaultConfig() cfg, err = config.LoadDefaultConfig()
} else if err != nil { }
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(2) if err != nil {
return err
} }
} }
if err = api.Generate(cfg); err != nil { if err = api.Generate(cfg); err != nil {
fmt.Fprintln(os.Stderr, err.Error()) return err
os.Exit(3)
} }
return nil
}, },
} }

View File

@@ -3,28 +3,79 @@ package cmd
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"html/template"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/99designs/gqlgen/api" "github.com/99designs/gqlgen/api"
"github.com/99designs/gqlgen/plugin/servergen"
"github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/config"
"github.com/pkg/errors" "github.com/99designs/gqlgen/internal/code"
"github.com/urfave/cli" "github.com/99designs/gqlgen/plugin/servergen"
yaml "gopkg.in/yaml.v2" "github.com/urfave/cli/v2"
) )
var configComment = ` var configTemplate = template.Must(template.New("name").Parse(
# .gqlgen.yml example `# Where are all the schema files located? globs are supported eg src/**/*.graphqls
# schema:
# Refer to https://gqlgen.com/config/ - graph/*.graphqls
# for detailed .gqlgen.yml documentation.
`
var schemaDefault = ` # Where should the generated server code go?
# GraphQL schema example exec:
filename: graph/generated/generated.go
package: generated
# Uncomment to enable federation
# federation:
# filename: graph/generated/federation.go
# package: generated
# Where should any generated models go?
model:
filename: graph/model/models_gen.go
package: model
# Where should the resolver implementations go?
resolver:
layout: follow-schema
dir: graph
package: graph
# Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models
# struct_tag: json
# Optional: turn on to use []Thing instead of []*Thing
# omit_slice_element_pointers: false
# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
autobind:
- "{{.}}/graph/model"
# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
ID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
Int:
model:
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
`))
var schemaDefault = `# GraphQL schema example
# #
# https://gqlgen.com/getting-started/ # https://gqlgen.com/getting-started/
@@ -54,91 +105,95 @@ type Mutation {
} }
` `
var initCmd = cli.Command{ var initCmd = &cli.Command{
Name: "init", Name: "init",
Usage: "create a new gqlgen project", Usage: "create a new gqlgen project",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.BoolFlag{Name: "verbose, v", Usage: "show logs"}, &cli.BoolFlag{Name: "verbose, v", Usage: "show logs"},
cli.StringFlag{Name: "config, c", Usage: "the config filename"}, &cli.StringFlag{Name: "config, c", Usage: "the config filename"},
cli.StringFlag{Name: "server", Usage: "where to write the server stub to", Value: "server/server.go"}, &cli.StringFlag{Name: "server", Usage: "where to write the server stub to", Value: "server.go"},
cli.StringFlag{Name: "schema", Usage: "where to write the schema stub to", Value: "schema.graphql"}, &cli.StringFlag{Name: "schema", Usage: "where to write the schema stub to", Value: "graph/schema.graphqls"},
}, },
Action: func(ctx *cli.Context) { Action: func(ctx *cli.Context) error {
initSchema(ctx.String("schema")) configFilename := ctx.String("config")
config := initConfig(ctx) serverFilename := ctx.String("server")
GenerateGraphServer(config, ctx.String("server")) pkgName := code.ImportPathForDir(".")
if pkgName == "" {
return fmt.Errorf("unable to determine import path for current directory, you probably need to run go mod init first")
}
if err := initSchema(ctx.String("schema")); err != nil {
return err
}
if !configExists(configFilename) {
if err := initConfig(configFilename, pkgName); err != nil {
return err
}
}
GenerateGraphServer(serverFilename)
return nil
}, },
} }
func GenerateGraphServer(cfg *config.Config, serverFilename string) { func GenerateGraphServer(serverFilename string) {
err := api.Generate(cfg, api.AddPlugin(servergen.New(serverFilename))) cfg, err := config.LoadConfigFromDefaultLocations()
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err.Error()) fmt.Fprintln(os.Stderr, err.Error())
} }
if err := api.Generate(cfg, api.AddPlugin(servergen.New(serverFilename))); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
fmt.Fprintf(os.Stdout, "Exec \"go run ./%s\" to start GraphQL server\n", serverFilename) fmt.Fprintf(os.Stdout, "Exec \"go run ./%s\" to start GraphQL server\n", serverFilename)
} }
func initConfig(ctx *cli.Context) *config.Config { func configExists(configFilename string) bool {
var cfg *config.Config var cfg *config.Config
var err error
configFilename := ctx.String("config")
if configFilename != "" { if configFilename != "" {
cfg, err = config.LoadConfig(configFilename) cfg, _ = config.LoadConfig(configFilename)
} else { } else {
cfg, err = config.LoadConfigFromDefaultLocations() cfg, _ = config.LoadConfigFromDefaultLocations()
}
if cfg != nil {
fmt.Fprintf(os.Stderr, "init failed: a configuration file already exists\n")
os.Exit(1)
}
if !os.IsNotExist(errors.Cause(err)) {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
} }
return cfg != nil
}
func initConfig(configFilename string, pkgName string) error {
if configFilename == "" { if configFilename == "" {
configFilename = "gqlgen.yml" configFilename = "gqlgen.yml"
} }
cfg = config.DefaultConfig()
cfg.Resolver = config.PackageConfig{ if err := os.MkdirAll(filepath.Dir(configFilename), 0755); err != nil {
Filename: "resolver.go", return fmt.Errorf("unable to create config dir: " + err.Error())
Type: "Resolver",
} }
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(strings.TrimSpace(configComment)) if err := configTemplate.Execute(&buf, pkgName); err != nil {
buf.WriteString("\n\n") panic(err)
var b []byte
b, err = yaml.Marshal(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, "unable to marshal yaml: "+err.Error())
os.Exit(1)
}
buf.Write(b)
err = ioutil.WriteFile(configFilename, buf.Bytes(), 0644)
if err != nil {
fmt.Fprintln(os.Stderr, "unable to write cfg file: "+err.Error())
os.Exit(1)
} }
return cfg if err := ioutil.WriteFile(configFilename, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("unable to write cfg file: " + err.Error())
}
return nil
} }
func initSchema(schemaFilename string) { func initSchema(schemaFilename string) error {
_, err := os.Stat(schemaFilename) _, err := os.Stat(schemaFilename)
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return return nil
} }
err = ioutil.WriteFile(schemaFilename, []byte(strings.TrimSpace(schemaDefault)), 0644) if err := os.MkdirAll(filepath.Dir(schemaFilename), 0755); err != nil {
if err != nil { return fmt.Errorf("unable to create schema dir: " + err.Error())
fmt.Fprintln(os.Stderr, "unable to write schema file: "+err.Error())
os.Exit(1)
} }
if err = ioutil.WriteFile(schemaFilename, []byte(strings.TrimSpace(schemaDefault)), 0644); err != nil {
return fmt.Errorf("unable to write schema file: " + err.Error())
}
return nil
} }

View File

@@ -7,9 +7,10 @@ import (
"os" "os"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
"github.com/urfave/cli" "github.com/urfave/cli/v2"
// Required since otherwise dep will prune away these unused packages before codegen has a chance to run // Required since otherwise dep will prune away these unused packages before codegen has a chance to run
_ "github.com/99designs/gqlgen/graphql/handler"
_ "github.com/99designs/gqlgen/handler" _ "github.com/99designs/gqlgen/handler"
) )
@@ -31,7 +32,7 @@ func Execute() {
} }
app.Action = genCmd.Action app.Action = genCmd.Action
app.Commands = []cli.Command{ app.Commands = []*cli.Command{
genCmd, genCmd,
initCmd, initCmd,
versionCmd, versionCmd,

View File

@@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
"github.com/urfave/cli" "github.com/urfave/cli/v2"
) )
var versionCmd = cli.Command{ var versionCmd = &cli.Command{
Name: "version", Name: "version",
Usage: "print the version string", Usage: "print the version string",
Action: func(ctx *cli.Context) { Action: func(ctx *cli.Context) error {
fmt.Println(graphql.Version) fmt.Println(graphql.Version)
return nil
}, },
} }

View File

@@ -8,7 +8,7 @@ import (
"github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/codegen/templates" "github.com/99designs/gqlgen/codegen/templates"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
) )
type ArgSet struct { type ArgSet struct {
@@ -26,6 +26,22 @@ type FieldArgument struct {
Value interface{} // value set in Data Value interface{} // value set in Data
} }
//ImplDirectives get not Builtin and location ARGUMENT_DEFINITION directive
func (f *FieldArgument) ImplDirectives() []*Directive {
d := make([]*Directive, 0)
for i := range f.Directives {
if !f.Directives[i].Builtin && f.Directives[i].IsLocation(ast.LocationArgumentDefinition) {
d = append(d, f.Directives[i])
}
}
return d
}
func (f *FieldArgument) DirectiveObjName() string {
return "rawArgs"
}
func (f *FieldArgument) Stream() bool { func (f *FieldArgument) Stream() bool {
return f.Object != nil && f.Object.Stream return f.Object != nil && f.Object.Stream
} }

View File

@@ -5,27 +5,20 @@ func (ec *executionContext) {{ $name }}(ctx context.Context, rawArgs map[string]
{{- range $i, $arg := . }} {{- range $i, $arg := . }}
var arg{{$i}} {{ $arg.TypeReference.GO | ref}} var arg{{$i}} {{ $arg.TypeReference.GO | ref}}
if tmp, ok := rawArgs[{{$arg.Name|quote}}]; ok { if tmp, ok := rawArgs[{{$arg.Name|quote}}]; ok {
{{- if $arg.Directives }} ctx := graphql.WithFieldInputContext(ctx, graphql.NewFieldInputWithField({{$arg.Name|quote}}))
getArg0 := func(ctx context.Context) (interface{}, error) { return ec.{{ $arg.TypeReference.UnmarshalFunc }}(ctx, tmp) } {{- if $arg.ImplDirectives }}
directive0 := func(ctx context.Context) (interface{}, error) { return ec.{{ $arg.TypeReference.UnmarshalFunc }}(ctx, tmp) }
{{- range $i, $directive := $arg.Directives }} {{ template "implDirectives" $arg }}
getArg{{add $i 1}} := func(ctx context.Context) (res interface{}, err error) { tmp, err = directive{{$arg.ImplDirectives|len}}(ctx)
{{- range $dArg := $directive.Args }}
{{- if and $dArg.TypeReference.IsPtr ( notNil "Value" $dArg ) }}
{{ $dArg.VarName }} := {{ $dArg.Value | dump }}
{{- end }}
{{- end }}
n := getArg{{$i}}
return ec.directives.{{$directive.Name|ucFirst}}({{$directive.ResolveArgs "tmp" "n" }})
}
{{- end }}
tmp, err = getArg{{$arg.Directives|len}}(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if data, ok := tmp.({{ $arg.TypeReference.GO | ref }}) ; ok { if data, ok := tmp.({{ $arg.TypeReference.GO | ref }}) ; ok {
arg{{$i}} = data arg{{$i}} = data
{{- if $arg.TypeReference.IsNilable }}
} else if tmp == nil {
arg{{$i}} = nil
{{- end }}
} else { } else {
return nil, fmt.Errorf(`unexpected type %T from directive, should be {{ $arg.TypeReference.GO }}`, tmp) return nil, fmt.Errorf(`unexpected type %T from directive, should be {{ $arg.TypeReference.GO }}`, tmp)
} }

View File

@@ -8,37 +8,24 @@ import (
"github.com/99designs/gqlgen/codegen/templates" "github.com/99designs/gqlgen/codegen/templates"
"github.com/99designs/gqlgen/internal/code" "github.com/99designs/gqlgen/internal/code"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
"golang.org/x/tools/go/packages"
) )
// Binder connects graphql types to golang types using static analysis // Binder connects graphql types to golang types using static analysis
type Binder struct { type Binder struct {
pkgs []*packages.Package pkgs *code.Packages
schema *ast.Schema schema *ast.Schema
cfg *Config cfg *Config
References []*TypeReference References []*TypeReference
SawInvalid bool
} }
func (c *Config) NewBinder(s *ast.Schema) (*Binder, error) { func (c *Config) NewBinder() *Binder {
pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadTypes | packages.LoadSyntax}, c.Models.ReferencedPackages()...)
if err != nil {
return nil, err
}
for _, p := range pkgs {
for _, e := range p.Errors {
if e.Kind == packages.ListError {
return nil, p.Errors[0]
}
}
}
return &Binder{ return &Binder{
pkgs: pkgs, pkgs: c.Packages,
schema: s, schema: c.Schema,
cfg: c, cfg: c,
}, nil }
} }
func (b *Binder) TypePosition(typ types.Type) token.Position { func (b *Binder) TypePosition(typ types.Type) token.Position {
@@ -58,11 +45,26 @@ func (b *Binder) ObjectPosition(typ types.Object) token.Position {
Filename: "unknown", Filename: "unknown",
} }
} }
pkg := b.getPkg(typ.Pkg().Path()) pkg := b.pkgs.Load(typ.Pkg().Path())
return pkg.Fset.Position(typ.Pos()) return pkg.Fset.Position(typ.Pos())
} }
func (b *Binder) FindTypeFromName(name string) (types.Type, error) {
pkgName, typeName := code.PkgAndType(name)
return b.FindType(pkgName, typeName)
}
func (b *Binder) FindType(pkgName string, typeName string) (types.Type, error) { func (b *Binder) FindType(pkgName string, typeName string) (types.Type, error) {
if pkgName == "" {
if typeName == "map[string]interface{}" {
return MapType, nil
}
if typeName == "interface{}" {
return InterfaceType, nil
}
}
obj, err := b.FindObject(pkgName, typeName) obj, err := b.FindObject(pkgName, typeName)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -74,15 +76,6 @@ func (b *Binder) FindType(pkgName string, typeName string) (types.Type, error) {
return obj.Type(), nil return obj.Type(), nil
} }
func (b *Binder) getPkg(find string) *packages.Package {
for _, p := range b.pkgs {
if code.NormalizeVendor(find) == code.NormalizeVendor(p.PkgPath) {
return p
}
}
return nil
}
var MapType = types.NewMap(types.Typ[types.String], types.NewInterfaceType(nil, nil).Complete()) var MapType = types.NewMap(types.Typ[types.String], types.NewInterfaceType(nil, nil).Complete())
var InterfaceType = types.NewInterfaceType(nil, nil) var InterfaceType = types.NewInterfaceType(nil, nil)
@@ -122,7 +115,7 @@ func (b *Binder) FindObject(pkgName string, typeName string) (types.Object, erro
fullName = pkgName + "." + typeName fullName = pkgName + "." + typeName
} }
pkg := b.getPkg(pkgName) pkg := b.pkgs.LoadWithTypes(pkgName)
if pkg == nil { if pkg == nil {
return nil, errors.Errorf("required package was not loaded: %s", fullName) return nil, errors.Errorf("required package was not loaded: %s", fullName)
} }
@@ -173,7 +166,8 @@ func (b *Binder) PointerTo(ref *TypeReference) *TypeReference {
type TypeReference struct { type TypeReference struct {
Definition *ast.Definition Definition *ast.Definition
GQL *ast.Type GQL *ast.Type
GO types.Type GO types.Type // Type of the field being bound. Could be a pointer or a value type of Target.
Target types.Type // The actual type that we know how to bind to. May require pointer juggling when traversing to fields.
CastType types.Type // Before calling marshalling functions cast from/to this base type CastType types.Type // Before calling marshalling functions cast from/to this base type
Marshaler *types.Func // When using external marshalling functions this will point to the Marshal function Marshaler *types.Func // When using external marshalling functions this will point to the Marshal function
Unmarshaler *types.Func // When using external marshalling functions this will point to the Unmarshal function Unmarshaler *types.Func // When using external marshalling functions this will point to the Unmarshal function
@@ -184,6 +178,7 @@ func (ref *TypeReference) Elem() *TypeReference {
if p, isPtr := ref.GO.(*types.Pointer); isPtr { if p, isPtr := ref.GO.(*types.Pointer); isPtr {
return &TypeReference{ return &TypeReference{
GO: p.Elem(), GO: p.Elem(),
Target: ref.Target,
GQL: ref.GQL, GQL: ref.GQL,
CastType: ref.CastType, CastType: ref.CastType,
Definition: ref.Definition, Definition: ref.Definition,
@@ -196,6 +191,7 @@ func (ref *TypeReference) Elem() *TypeReference {
if ref.IsSlice() { if ref.IsSlice() {
return &TypeReference{ return &TypeReference{
GO: ref.GO.(*types.Slice).Elem(), GO: ref.GO.(*types.Slice).Elem(),
Target: ref.Target,
GQL: ref.GQL.Elem, GQL: ref.GQL.Elem,
CastType: ref.CastType, CastType: ref.CastType,
Definition: ref.Definition, Definition: ref.Definition,
@@ -213,10 +209,7 @@ func (t *TypeReference) IsPtr() bool {
} }
func (t *TypeReference) IsNilable() bool { func (t *TypeReference) IsNilable() bool {
_, isPtr := t.GO.(*types.Pointer) return IsNilable(t.GO)
_, isMap := t.GO.(*types.Map)
_, isInterface := t.GO.(*types.Interface)
return isPtr || isMap || isInterface
} }
func (t *TypeReference) IsSlice() bool { func (t *TypeReference) IsSlice() bool {
@@ -244,7 +237,12 @@ func (t *TypeReference) UniquenessKey() string {
nullability = "N" nullability = "N"
} }
return nullability + t.Definition.Name + "2" + templates.TypeIdentifier(t.GO) var elemNullability = ""
if t.GQL.Elem != nil && t.GQL.Elem.NonNull {
// Fix for #896
elemNullability = "ᚄ"
}
return nullability + t.Definition.Name + "2" + templates.TypeIdentifier(t.GO) + elemNullability
} }
func (t *TypeReference) MarshalFunc() string { func (t *TypeReference) MarshalFunc() string {
@@ -271,6 +269,10 @@ func (t *TypeReference) UnmarshalFunc() string {
return "unmarshal" + t.UniquenessKey() return "unmarshal" + t.UniquenessKey()
} }
func (t *TypeReference) IsTargetNilable() bool {
return IsNilable(t.Target)
}
func (b *Binder) PushRef(ret *TypeReference) { func (b *Binder) PushRef(ret *TypeReference) {
b.References = append(b.References, ret) b.References = append(b.References, ret)
} }
@@ -292,6 +294,11 @@ func isIntf(t types.Type) bool {
} }
func (b *Binder) TypeReference(schemaType *ast.Type, bindTarget types.Type) (ret *TypeReference, err error) { func (b *Binder) TypeReference(schemaType *ast.Type, bindTarget types.Type) (ret *TypeReference, err error) {
if !isValid(bindTarget) {
b.SawInvalid = true
return nil, fmt.Errorf("%s has an invalid type", schemaType.Name())
}
var pkgName, typeName string var pkgName, typeName string
def := b.schema.Types[schemaType.Name()] def := b.schema.Types[schemaType.Name()]
defer func() { defer func() {
@@ -350,7 +357,7 @@ func (b *Binder) TypeReference(schemaType *ast.Type, bindTarget types.Type) (ret
ref.GO = obj.Type() ref.GO = obj.Type()
ref.IsMarshaler = true ref.IsMarshaler = true
} else if underlying := basicUnderlying(obj.Type()); def.IsLeafType() && underlying != nil && underlying.Kind() == types.String { } else if underlying := basicUnderlying(obj.Type()); def.IsLeafType() && underlying != nil && underlying.Kind() == types.String {
// Special case for named types wrapping strings. Used by default enum implementations. // TODO delete before v1. Backwards compatibility case for named types wrapping strings (see #595)
ref.GO = obj.Type() ref.GO = obj.Type()
ref.CastType = underlying ref.CastType = underlying
@@ -366,6 +373,7 @@ func (b *Binder) TypeReference(schemaType *ast.Type, bindTarget types.Type) (ret
ref.GO = obj.Type() ref.GO = obj.Type()
} }
ref.Target = ref.GO
ref.GO = b.CopyModifiersFromAst(schemaType, ref.GO) ref.GO = b.CopyModifiersFromAst(schemaType, ref.GO)
if bindTarget != nil { if bindTarget != nil {
@@ -378,13 +386,21 @@ func (b *Binder) TypeReference(schemaType *ast.Type, bindTarget types.Type) (ret
return ref, nil return ref, nil
} }
return nil, fmt.Errorf("%s has type compatible with %s", schemaType.Name(), bindTarget.String()) return nil, fmt.Errorf("%s is incompatible with %s", schemaType.Name(), bindTarget.String())
}
func isValid(t types.Type) bool {
basic, isBasic := t.(*types.Basic)
if !isBasic {
return true
}
return basic.Kind() != types.Invalid
} }
func (b *Binder) CopyModifiersFromAst(t *ast.Type, base types.Type) types.Type { func (b *Binder) CopyModifiersFromAst(t *ast.Type, base types.Type) types.Type {
if t.Elem != nil { if t.Elem != nil {
child := b.CopyModifiersFromAst(t.Elem, base) child := b.CopyModifiersFromAst(t.Elem, base)
if _, isStruct := child.Underlying().(*types.Struct); isStruct { if _, isStruct := child.Underlying().(*types.Struct); isStruct && !b.cfg.OmitSliceElementPointers {
child = types.NewPointer(child) child = types.NewPointer(child)
} }
return types.NewSlice(child) return types.NewSlice(child)
@@ -395,13 +411,25 @@ func (b *Binder) CopyModifiersFromAst(t *ast.Type, base types.Type) types.Type {
_, isInterface = named.Underlying().(*types.Interface) _, isInterface = named.Underlying().(*types.Interface)
} }
if !isInterface && !t.NonNull { if !isInterface && !IsNilable(base) && !t.NonNull {
return types.NewPointer(base) return types.NewPointer(base)
} }
return base return base
} }
func IsNilable(t types.Type) bool {
if namedType, isNamed := t.(*types.Named); isNamed {
return IsNilable(namedType.Underlying())
}
_, isPtr := t.(*types.Pointer)
_, isMap := t.(*types.Map)
_, isInterface := t.(*types.Interface)
_, isSlice := t.(*types.Slice)
_, isChan := t.(*types.Chan)
return isPtr || isMap || isInterface || isSlice || isChan
}
func hasMethod(it types.Type, name string) bool { func hasMethod(it types.Type, name string) bool {
if ptr, isPtr := it.(*types.Pointer); isPtr { if ptr, isPtr := it.(*types.Pointer); isPtr {
it = ptr.Elem() it = ptr.Elem()

View File

@@ -2,27 +2,38 @@ package config
import ( import (
"fmt" "fmt"
"go/types"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strings" "strings"
"github.com/99designs/gqlgen/internal/code" "github.com/99designs/gqlgen/internal/code"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/vektah/gqlparser" "github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
yaml "gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
type Config struct { type Config struct {
SchemaFilename StringList `yaml:"schema,omitempty"` SchemaFilename StringList `yaml:"schema,omitempty"`
Exec PackageConfig `yaml:"exec"` Exec PackageConfig `yaml:"exec"`
Model PackageConfig `yaml:"model"` Model PackageConfig `yaml:"model,omitempty"`
Resolver PackageConfig `yaml:"resolver,omitempty"` Federation PackageConfig `yaml:"federation,omitempty"`
Models TypeMap `yaml:"models,omitempty"` Resolver ResolverConfig `yaml:"resolver,omitempty"`
StructTag string `yaml:"struct_tag,omitempty"` AutoBind []string `yaml:"autobind"`
Models TypeMap `yaml:"models,omitempty"`
StructTag string `yaml:"struct_tag,omitempty"`
Directives map[string]DirectiveConfig `yaml:"directives,omitempty"`
OmitSliceElementPointers bool `yaml:"omit_slice_element_pointers,omitempty"`
SkipValidation bool `yaml:"skip_validation,omitempty"`
Sources []*ast.Source `yaml:"-"`
Packages *code.Packages `yaml:"-"`
Schema *ast.Schema `yaml:"-"`
// Deprecated use Federation instead. Will be removed next release
Federated bool `yaml:"federated,omitempty"`
} }
var cfgFilenames = []string{".gqlgen.yml", "gqlgen.yml", "gqlgen.yaml"} var cfgFilenames = []string{".gqlgen.yml", "gqlgen.yml", "gqlgen.yaml"}
@@ -33,9 +44,30 @@ func DefaultConfig() *Config {
SchemaFilename: StringList{"schema.graphql"}, SchemaFilename: StringList{"schema.graphql"},
Model: PackageConfig{Filename: "models_gen.go"}, Model: PackageConfig{Filename: "models_gen.go"},
Exec: PackageConfig{Filename: "generated.go"}, Exec: PackageConfig{Filename: "generated.go"},
Directives: map[string]DirectiveConfig{},
Models: TypeMap{},
} }
} }
// LoadDefaultConfig loads the default config so that it is ready to be used
func LoadDefaultConfig() (*Config, error) {
config := DefaultConfig()
for _, filename := range config.SchemaFilename {
filename = filepath.ToSlash(filename)
var err error
var schemaRaw []byte
schemaRaw, err = ioutil.ReadFile(filename)
if err != nil {
return nil, errors.Wrap(err, "unable to open schema")
}
config.Sources = append(config.Sources, &ast.Source{Name: filename, Input: string(schemaRaw)})
}
return config, nil
}
// LoadConfigFromDefaultLocations looks for a config file in the current directory, and all parent directories // LoadConfigFromDefaultLocations looks for a config file in the current directory, and all parent directories
// walking up the tree. The closest config file will be returned. // walking up the tree. The closest config file will be returned.
func LoadConfigFromDefaultLocations() (*Config, error) { func LoadConfigFromDefaultLocations() (*Config, error) {
@@ -51,6 +83,13 @@ func LoadConfigFromDefaultLocations() (*Config, error) {
return LoadConfig(cfgFile) return LoadConfig(cfgFile)
} }
var path2regex = strings.NewReplacer(
`.`, `\.`,
`*`, `.+`,
`\`, `[\\/]`,
`/`, `[\\/]`,
)
// LoadConfig reads the gqlgen.yml config file // LoadConfig reads the gqlgen.yml config file
func LoadConfig(filename string) (*Config, error) { func LoadConfig(filename string) (*Config, error) {
config := DefaultConfig() config := DefaultConfig()
@@ -64,12 +103,50 @@ func LoadConfig(filename string) (*Config, error) {
return nil, errors.Wrap(err, "unable to parse config") return nil, errors.Wrap(err, "unable to parse config")
} }
defaultDirectives := map[string]DirectiveConfig{
"skip": {SkipRuntime: true},
"include": {SkipRuntime: true},
"deprecated": {SkipRuntime: true},
}
for key, value := range defaultDirectives {
if _, defined := config.Directives[key]; !defined {
config.Directives[key] = value
}
}
preGlobbing := config.SchemaFilename preGlobbing := config.SchemaFilename
config.SchemaFilename = StringList{} config.SchemaFilename = StringList{}
for _, f := range preGlobbing { for _, f := range preGlobbing {
matches, err := filepath.Glob(f) var matches []string
if err != nil {
return nil, errors.Wrapf(err, "failed to glob schema filename %s", f) // for ** we want to override default globbing patterns and walk all
// subdirectories to match schema files.
if strings.Contains(f, "**") {
pathParts := strings.SplitN(f, "**", 2)
rest := strings.TrimPrefix(strings.TrimPrefix(pathParts[1], `\`), `/`)
// turn the rest of the glob into a regex, anchored only at the end because ** allows
// for any number of dirs in between and walk will let us match against the full path name
globRe := regexp.MustCompile(path2regex.Replace(rest) + `$`)
if err := filepath.Walk(pathParts[0], func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if globRe.MatchString(strings.TrimPrefix(path, pathParts[0])) {
matches = append(matches, path)
}
return nil
}); err != nil {
return nil, errors.Wrapf(err, "failed to walk schema at root %s", pathParts[0])
}
} else {
matches, err = filepath.Glob(f)
if err != nil {
return nil, errors.Wrapf(err, "failed to glob schema filename %s", f)
}
} }
for _, m := range matches { for _, m := range matches {
@@ -80,13 +157,126 @@ func LoadConfig(filename string) (*Config, error) {
} }
} }
for _, filename := range config.SchemaFilename {
filename = filepath.ToSlash(filename)
var err error
var schemaRaw []byte
schemaRaw, err = ioutil.ReadFile(filename)
if err != nil {
return nil, errors.Wrap(err, "unable to open schema")
}
config.Sources = append(config.Sources, &ast.Source{Name: filename, Input: string(schemaRaw)})
}
return config, nil return config, nil
} }
type PackageConfig struct { func (c *Config) Init() error {
Filename string `yaml:"filename,omitempty"` if c.Packages == nil {
Package string `yaml:"package,omitempty"` c.Packages = &code.Packages{}
Type string `yaml:"type,omitempty"` }
if c.Schema == nil {
if err := c.LoadSchema(); err != nil {
return err
}
}
err := c.injectTypesFromSchema()
if err != nil {
return err
}
err = c.autobind()
if err != nil {
return err
}
c.injectBuiltins()
// prefetch all packages in one big packages.Load call
pkgs := []string{
"github.com/99designs/gqlgen/graphql",
"github.com/99designs/gqlgen/graphql/introspection",
}
pkgs = append(pkgs, c.Models.ReferencedPackages()...)
pkgs = append(pkgs, c.AutoBind...)
c.Packages.LoadAll(pkgs...)
// check everything is valid on the way out
err = c.check()
if err != nil {
return err
}
return nil
}
func (c *Config) injectTypesFromSchema() error {
c.Directives["goModel"] = DirectiveConfig{
SkipRuntime: true,
}
c.Directives["goField"] = DirectiveConfig{
SkipRuntime: true,
}
for _, schemaType := range c.Schema.Types {
if schemaType == c.Schema.Query || schemaType == c.Schema.Mutation || schemaType == c.Schema.Subscription {
continue
}
if bd := schemaType.Directives.ForName("goModel"); bd != nil {
if ma := bd.Arguments.ForName("model"); ma != nil {
if mv, err := ma.Value.Value(nil); err == nil {
c.Models.Add(schemaType.Name, mv.(string))
}
}
if ma := bd.Arguments.ForName("models"); ma != nil {
if mvs, err := ma.Value.Value(nil); err == nil {
for _, mv := range mvs.([]interface{}) {
c.Models.Add(schemaType.Name, mv.(string))
}
}
}
}
if schemaType.Kind == ast.Object || schemaType.Kind == ast.InputObject {
for _, field := range schemaType.Fields {
if fd := field.Directives.ForName("goField"); fd != nil {
forceResolver := c.Models[schemaType.Name].Fields[field.Name].Resolver
fieldName := c.Models[schemaType.Name].Fields[field.Name].FieldName
if ra := fd.Arguments.ForName("forceResolver"); ra != nil {
if fr, err := ra.Value.Value(nil); err == nil {
forceResolver = fr.(bool)
}
}
if na := fd.Arguments.ForName("name"); na != nil {
if fr, err := na.Value.Value(nil); err == nil {
fieldName = fr.(string)
}
}
if c.Models[schemaType.Name].Fields == nil {
c.Models[schemaType.Name] = TypeMapEntry{
Model: c.Models[schemaType.Name].Model,
Fields: map[string]TypeMapField{},
}
}
c.Models[schemaType.Name].Fields[field.Name] = TypeMapField{
FieldName: fieldName,
Resolver: forceResolver,
}
}
}
}
}
return nil
} }
type TypeMapEntry struct { type TypeMapEntry struct {
@@ -95,8 +285,9 @@ type TypeMapEntry struct {
} }
type TypeMapField struct { type TypeMapField struct {
Resolver bool `yaml:"resolver"` Resolver bool `yaml:"resolver"`
FieldName string `yaml:"fieldName"` FieldName string `yaml:"fieldName"`
GeneratedMethod string `yaml:"-"`
} }
type StringList []string type StringList []string
@@ -128,90 +319,85 @@ func (a StringList) Has(file string) bool {
return false return false
} }
func (c *PackageConfig) normalize() error { func (c *Config) check() error {
if c.Filename == "" { if c.Models == nil {
return errors.New("Filename is required") c.Models = TypeMap{}
}
c.Filename = abs(c.Filename)
// If Package is not set, first attempt to load the package at the output dir. If that fails
// fallback to just the base dir name of the output filename.
if c.Package == "" {
c.Package = code.NameForDir(c.Dir())
} }
return nil type FilenamePackage struct {
} Filename string
Package string
func (c *PackageConfig) ImportPath() string { Declaree string
return code.ImportPathForDir(c.Dir())
}
func (c *PackageConfig) Dir() string {
return filepath.Dir(c.Filename)
}
func (c *PackageConfig) Check() error {
if strings.ContainsAny(c.Package, "./\\") {
return fmt.Errorf("package should be the output package name only, do not include the output filename")
}
if c.Filename != "" && !strings.HasSuffix(c.Filename, ".go") {
return fmt.Errorf("filename should be path to a go source file")
} }
return c.normalize() fileList := map[string][]FilenamePackage{}
}
func (c *PackageConfig) Pkg() *types.Package {
return types.NewPackage(c.ImportPath(), c.Dir())
}
func (c *PackageConfig) IsDefined() bool {
return c.Filename != ""
}
func (c *Config) Check() error {
if err := c.Models.Check(); err != nil { if err := c.Models.Check(); err != nil {
return errors.Wrap(err, "config.models") return errors.Wrap(err, "config.models")
} }
if err := c.Exec.Check(); err != nil { if err := c.Exec.Check(); err != nil {
return errors.Wrap(err, "config.exec") return errors.Wrap(err, "config.exec")
} }
if err := c.Model.Check(); err != nil { fileList[c.Exec.ImportPath()] = append(fileList[c.Exec.ImportPath()], FilenamePackage{
return errors.Wrap(err, "config.model") Filename: c.Exec.Filename,
Package: c.Exec.Package,
Declaree: "exec",
})
if c.Model.IsDefined() {
if err := c.Model.Check(); err != nil {
return errors.Wrap(err, "config.model")
}
fileList[c.Model.ImportPath()] = append(fileList[c.Model.ImportPath()], FilenamePackage{
Filename: c.Model.Filename,
Package: c.Model.Package,
Declaree: "model",
})
} }
if c.Resolver.IsDefined() { if c.Resolver.IsDefined() {
if err := c.Resolver.Check(); err != nil { if err := c.Resolver.Check(); err != nil {
return errors.Wrap(err, "config.resolver") return errors.Wrap(err, "config.resolver")
} }
fileList[c.Resolver.ImportPath()] = append(fileList[c.Resolver.ImportPath()], FilenamePackage{
Filename: c.Resolver.Filename,
Package: c.Resolver.Package,
Declaree: "resolver",
})
} }
if c.Federation.IsDefined() {
// check packages names against conflict, if present in the same dir if err := c.Federation.Check(); err != nil {
// and check filenames for uniqueness return errors.Wrap(err, "config.federation")
packageConfigList := []PackageConfig{
c.Model,
c.Exec,
c.Resolver,
}
filesMap := make(map[string]bool)
pkgConfigsByDir := make(map[string]PackageConfig)
for _, current := range packageConfigList {
_, fileFound := filesMap[current.Filename]
if fileFound {
return fmt.Errorf("filename %s defined more than once", current.Filename)
} }
filesMap[current.Filename] = true fileList[c.Federation.ImportPath()] = append(fileList[c.Federation.ImportPath()], FilenamePackage{
previous, inSameDir := pkgConfigsByDir[current.Dir()] Filename: c.Federation.Filename,
if inSameDir && current.Package != previous.Package { Package: c.Federation.Package,
return fmt.Errorf("filenames %s and %s are in the same directory but have different package definitions", stripPath(current.Filename), stripPath(previous.Filename)) Declaree: "federation",
})
if c.Federation.ImportPath() != c.Exec.ImportPath() {
return fmt.Errorf("federation and exec must be in the same package")
} }
pkgConfigsByDir[current.Dir()] = current }
if c.Federated {
return fmt.Errorf("federated has been removed, instead use\nfederation:\n filename: path/to/federated.go")
} }
return c.normalize() for importPath, pkg := range fileList {
} for _, file1 := range pkg {
for _, file2 := range pkg {
if file1.Package != file2.Package {
return fmt.Errorf("%s and %s define the same import path (%s) with different package names (%s vs %s)",
file1.Declaree,
file2.Declaree,
importPath,
file1.Package,
file2.Package,
)
}
}
}
}
func stripPath(path string) string { return nil
return filepath.Base(path)
} }
type TypeMap map[string]TypeMapEntry type TypeMap map[string]TypeMapEntry
@@ -259,10 +445,14 @@ func (tm TypeMap) ReferencedPackages() []string {
return pkgs return pkgs
} }
func (tm TypeMap) Add(Name string, goType string) { func (tm TypeMap) Add(name string, goType string) {
modelCfg := tm[Name] modelCfg := tm[name]
modelCfg.Model = append(modelCfg.Model, goType) modelCfg.Model = append(modelCfg.Model, goType)
tm[Name] = modelCfg tm[name] = modelCfg
}
type DirectiveConfig struct {
SkipRuntime bool `yaml:"skip_runtime"`
} }
func inStrSlice(haystack []string, needle string) bool { func inStrSlice(haystack []string, needle string) bool {
@@ -307,29 +497,54 @@ func findCfgInDir(dir string) string {
return "" return ""
} }
func (c *Config) normalize() error { func (c *Config) autobind() error {
if err := c.Model.normalize(); err != nil { if len(c.AutoBind) == 0 {
return errors.Wrap(err, "model") return nil
} }
if err := c.Exec.normalize(); err != nil { ps := c.Packages.LoadAll(c.AutoBind...)
return errors.Wrap(err, "exec")
}
if c.Resolver.IsDefined() { for _, t := range c.Schema.Types {
if err := c.Resolver.normalize(); err != nil { if c.Models.UserDefined(t.Name) {
return errors.Wrap(err, "resolver") continue
}
for i, p := range ps {
if p == nil {
return fmt.Errorf("unable to load %s - make sure you're using an import path to a package that exists", c.AutoBind[i])
}
if t := p.Types.Scope().Lookup(t.Name); t != nil {
c.Models.Add(t.Name(), t.Pkg().Path()+"."+t.Name())
break
}
} }
} }
if c.Models == nil { for i, t := range c.Models {
c.Models = TypeMap{} for j, m := range t.Model {
pkg, typename := code.PkgAndType(m)
// skip anything that looks like an import path
if strings.Contains(pkg, "/") {
continue
}
for _, p := range ps {
if p.Name != pkg {
continue
}
if t := p.Types.Scope().Lookup(typename); t != nil {
c.Models[i].Model[j] = t.Pkg().Path() + "." + t.Name()
break
}
}
}
} }
return nil return nil
} }
func (c *Config) InjectBuiltins(s *ast.Schema) { func (c *Config) injectBuiltins() {
builtins := TypeMap{ builtins := TypeMap{
"__Directive": {Model: StringList{"github.com/99designs/gqlgen/graphql/introspection.Directive"}}, "__Directive": {Model: StringList{"github.com/99designs/gqlgen/graphql/introspection.Directive"}},
"__DirectiveLocation": {Model: StringList{"github.com/99designs/gqlgen/graphql.String"}}, "__DirectiveLocation": {Model: StringList{"github.com/99designs/gqlgen/graphql.String"}},
@@ -370,35 +585,36 @@ func (c *Config) InjectBuiltins(s *ast.Schema) {
} }
for typeName, entry := range extraBuiltins { for typeName, entry := range extraBuiltins {
if t, ok := s.Types[typeName]; !c.Models.Exists(typeName) && ok && t.Kind == ast.Scalar { if t, ok := c.Schema.Types[typeName]; !c.Models.Exists(typeName) && ok && t.Kind == ast.Scalar {
c.Models[typeName] = entry c.Models[typeName] = entry
} }
} }
} }
func (c *Config) LoadSchema() (*ast.Schema, map[string]string, error) { func (c *Config) LoadSchema() error {
schemaStrings := map[string]string{} if c.Packages != nil {
c.Packages = &code.Packages{}
var sources []*ast.Source
for _, filename := range c.SchemaFilename {
filename = filepath.ToSlash(filename)
var err error
var schemaRaw []byte
schemaRaw, err = ioutil.ReadFile(filename)
if err != nil {
fmt.Fprintln(os.Stderr, "unable to open schema: "+err.Error())
os.Exit(1)
}
schemaStrings[filename] = string(schemaRaw)
sources = append(sources, &ast.Source{Name: filename, Input: schemaStrings[filename]})
} }
schema, err := gqlparser.LoadSchema(sources...) if err := c.check(); err != nil {
return err
}
schema, err := gqlparser.LoadSchema(c.Sources...)
if err != nil { if err != nil {
return nil, nil, err return err
} }
return schema, schemaStrings, nil
if schema.Query == nil {
schema.Query = &ast.Definition{
Kind: ast.Object,
Name: "Query",
}
schema.Types["Query"] = schema.Query
}
c.Schema = schema
return nil
} }
func abs(path string) string { func abs(path string) string {

View File

@@ -0,0 +1,62 @@
package config
import (
"fmt"
"go/types"
"path/filepath"
"strings"
"github.com/99designs/gqlgen/internal/code"
)
type PackageConfig struct {
Filename string `yaml:"filename,omitempty"`
Package string `yaml:"package,omitempty"`
}
func (c *PackageConfig) ImportPath() string {
if !c.IsDefined() {
return ""
}
return code.ImportPathForDir(c.Dir())
}
func (c *PackageConfig) Dir() string {
if !c.IsDefined() {
return ""
}
return filepath.Dir(c.Filename)
}
func (c *PackageConfig) Pkg() *types.Package {
if !c.IsDefined() {
return nil
}
return types.NewPackage(c.ImportPath(), c.Package)
}
func (c *PackageConfig) IsDefined() bool {
return c.Filename != ""
}
func (c *PackageConfig) Check() error {
if strings.ContainsAny(c.Package, "./\\") {
return fmt.Errorf("package should be the output package name only, do not include the output filename")
}
if c.Filename == "" {
return fmt.Errorf("filename must be specified")
}
if !strings.HasSuffix(c.Filename, ".go") {
return fmt.Errorf("filename should be path to a go source file")
}
c.Filename = abs(c.Filename)
// If Package is not set, first attempt to load the package at the output dir. If that fails
// fallback to just the base dir name of the output filename.
if c.Package == "" {
c.Package = code.NameForDir(c.Dir())
}
return nil
}

View File

@@ -0,0 +1,100 @@
package config
import (
"fmt"
"go/types"
"path/filepath"
"strings"
"github.com/99designs/gqlgen/internal/code"
)
type ResolverConfig struct {
Filename string `yaml:"filename,omitempty"`
FilenameTemplate string `yaml:"filename_template,omitempty"`
Package string `yaml:"package,omitempty"`
Type string `yaml:"type,omitempty"`
Layout ResolverLayout `yaml:"layout,omitempty"`
DirName string `yaml:"dir"`
}
type ResolverLayout string
var (
LayoutSingleFile ResolverLayout = "single-file"
LayoutFollowSchema ResolverLayout = "follow-schema"
)
func (r *ResolverConfig) Check() error {
if r.Layout == "" {
r.Layout = LayoutSingleFile
}
if r.Type == "" {
r.Type = "Resolver"
}
switch r.Layout {
case LayoutSingleFile:
if r.Filename == "" {
return fmt.Errorf("filename must be specified with layout=%s", r.Layout)
}
if !strings.HasSuffix(r.Filename, ".go") {
return fmt.Errorf("filename should be path to a go source file with layout=%s", r.Layout)
}
r.Filename = abs(r.Filename)
case LayoutFollowSchema:
if r.DirName == "" {
return fmt.Errorf("dirname must be specified with layout=%s", r.Layout)
}
r.DirName = abs(r.DirName)
if r.Filename == "" {
r.Filename = filepath.Join(r.DirName, "resolver.go")
} else {
r.Filename = abs(r.Filename)
}
default:
return fmt.Errorf("invalid layout %s. must be %s or %s", r.Layout, LayoutSingleFile, LayoutFollowSchema)
}
if strings.ContainsAny(r.Package, "./\\") {
return fmt.Errorf("package should be the output package name only, do not include the output filename")
}
if r.Package == "" && r.Dir() != "" {
r.Package = code.NameForDir(r.Dir())
}
return nil
}
func (r *ResolverConfig) ImportPath() string {
if r.Dir() == "" {
return ""
}
return code.ImportPathForDir(r.Dir())
}
func (r *ResolverConfig) Dir() string {
switch r.Layout {
case LayoutSingleFile:
if r.Filename == "" {
return ""
}
return filepath.Dir(r.Filename)
case LayoutFollowSchema:
return r.DirName
default:
panic("invalid layout " + r.Layout)
}
}
func (r *ResolverConfig) Pkg() *types.Package {
if r.Dir() == "" {
return nil
}
return types.NewPackage(r.ImportPath(), r.Package)
}
func (r *ResolverConfig) IsDefined() bool {
return r.Filename != "" || r.DirName != ""
}

View File

@@ -4,9 +4,10 @@ import (
"fmt" "fmt"
"sort" "sort"
"github.com/99designs/gqlgen/codegen/config"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
"github.com/99designs/gqlgen/codegen/config"
) )
// Data is a unified model of the code to be generated. Plugins may modify this structure to do things like implement // Data is a unified model of the code to be generated. Plugins may modify this structure to do things like implement
@@ -14,8 +15,7 @@ import (
type Data struct { type Data struct {
Config *config.Config Config *config.Config
Schema *ast.Schema Schema *ast.Schema
SchemaStr map[string]string Directives DirectiveList
Directives map[string]*Directive
Objects Objects Objects Objects
Inputs Objects Inputs Objects
Interfaces map[string]*Interface Interfaces map[string]*Interface
@@ -30,7 +30,6 @@ type Data struct {
type builder struct { type builder struct {
Config *config.Config Config *config.Config
Schema *ast.Schema Schema *ast.Schema
SchemaStr map[string]string
Binder *config.Binder Binder *config.Binder
Directives map[string]*Directive Directives map[string]*Directive
} }
@@ -38,26 +37,12 @@ type builder struct {
func BuildData(cfg *config.Config) (*Data, error) { func BuildData(cfg *config.Config) (*Data, error) {
b := builder{ b := builder{
Config: cfg, Config: cfg,
Schema: cfg.Schema,
} }
b.Binder = b.Config.NewBinder()
var err error var err error
b.Schema, b.SchemaStr, err = cfg.LoadSchema()
if err != nil {
return nil, err
}
err = cfg.Check()
if err != nil {
return nil, err
}
cfg.InjectBuiltins(b.Schema)
b.Binder, err = b.Config.NewBinder(b.Schema)
if err != nil {
return nil, err
}
b.Directives, err = b.buildDirectives() b.Directives, err = b.buildDirectives()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -74,7 +59,6 @@ func BuildData(cfg *config.Config) (*Data, error) {
Config: cfg, Config: cfg,
Directives: dataDirectives, Directives: dataDirectives,
Schema: b.Schema, Schema: b.Schema,
SchemaStr: b.SchemaStr,
Interfaces: map[string]*Interface{}, Interfaces: map[string]*Interface{},
} }
@@ -96,7 +80,10 @@ func BuildData(cfg *config.Config) (*Data, error) {
s.Inputs = append(s.Inputs, input) s.Inputs = append(s.Inputs, input)
case ast.Union, ast.Interface: case ast.Union, ast.Interface:
s.Interfaces[schemaType.Name] = b.buildInterface(schemaType) s.Interfaces[schemaType.Name], err = b.buildInterface(schemaType)
if err != nil {
return nil, errors.Wrap(err, "unable to bind to interface")
}
} }
} }
@@ -118,10 +105,7 @@ func BuildData(cfg *config.Config) (*Data, error) {
return nil, err return nil, err
} }
s.ReferencedTypes, err = b.buildTypes() s.ReferencedTypes = b.buildTypes()
if err != nil {
return nil, err
}
sort.Slice(s.Objects, func(i, j int) bool { sort.Slice(s.Objects, func(i, j int) bool {
return s.Objects[i].Definition.Name < s.Objects[j].Definition.Name return s.Objects[i].Definition.Name < s.Objects[j].Definition.Name
@@ -131,6 +115,17 @@ func BuildData(cfg *config.Config) (*Data, error) {
return s.Inputs[i].Definition.Name < s.Inputs[j].Definition.Name return s.Inputs[i].Definition.Name < s.Inputs[j].Definition.Name
}) })
if b.Binder.SawInvalid {
// if we have a syntax error, show it
err := cfg.Packages.Errors()
if len(err) > 0 {
return nil, err
}
// otherwise show a generic error message
return nil, fmt.Errorf("invalid types were encountered while traversing the go source code, this probably means the invalid code generated isnt correct. add try adding -v to debug")
}
return &s, nil return &s, nil
} }

View File

@@ -7,15 +7,46 @@ import (
"github.com/99designs/gqlgen/codegen/templates" "github.com/99designs/gqlgen/codegen/templates"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
) )
type DirectiveList map[string]*Directive
//LocationDirectives filter directives by location
func (dl DirectiveList) LocationDirectives(location string) DirectiveList {
return locationDirectives(dl, ast.DirectiveLocation(location))
}
type Directive struct { type Directive struct {
*ast.DirectiveDefinition
Name string Name string
Args []*FieldArgument Args []*FieldArgument
Builtin bool Builtin bool
} }
//IsLocation check location directive
func (d *Directive) IsLocation(location ...ast.DirectiveLocation) bool {
for _, l := range d.Locations {
for _, a := range location {
if l == a {
return true
}
}
}
return false
}
func locationDirectives(directives DirectiveList, location ...ast.DirectiveLocation) map[string]*Directive {
mDirectives := make(map[string]*Directive)
for name, d := range directives {
if d.IsLocation(location...) {
mDirectives[name] = d
}
}
return mDirectives
}
func (b *builder) buildDirectives() (map[string]*Directive, error) { func (b *builder) buildDirectives() (map[string]*Directive, error) {
directives := make(map[string]*Directive, len(b.Schema.Directives)) directives := make(map[string]*Directive, len(b.Schema.Directives))
@@ -24,11 +55,6 @@ func (b *builder) buildDirectives() (map[string]*Directive, error) {
return nil, errors.Errorf("directive with name %s already exists", name) return nil, errors.Errorf("directive with name %s already exists", name)
} }
var builtin bool
if name == "skip" || name == "include" || name == "deprecated" {
builtin = true
}
var args []*FieldArgument var args []*FieldArgument
for _, arg := range dir.Arguments { for _, arg := range dir.Arguments {
tr, err := b.Binder.TypeReference(arg.Type, nil) tr, err := b.Binder.TypeReference(arg.Type, nil)
@@ -53,9 +79,10 @@ func (b *builder) buildDirectives() (map[string]*Directive, error) {
} }
directives[name] = &Directive{ directives[name] = &Directive{
Name: name, DirectiveDefinition: dir,
Args: args, Name: name,
Builtin: builtin, Args: args,
Builtin: b.Config.Directives[name].SkipRuntime,
} }
} }
@@ -92,8 +119,10 @@ func (b *builder) getDirectives(list ast.DirectiveList) ([]*Directive, error) {
}) })
} }
dirs[i] = &Directive{ dirs[i] = &Directive{
Name: d.Name, Name: d.Name,
Args: args, Args: args,
DirectiveDefinition: list[i].Definition,
Builtin: b.Config.Directives[d.Name].SkipRuntime,
} }
} }
@@ -119,18 +148,12 @@ func (d *Directive) CallArgs() string {
return strings.Join(args, ", ") return strings.Join(args, ", ")
} }
func (d *Directive) ResolveArgs(obj string, next string) string { func (d *Directive) ResolveArgs(obj string, next int) string {
args := []string{"ctx", obj, next} args := []string{"ctx", obj, fmt.Sprintf("directive%d", next)}
for _, arg := range d.Args { for _, arg := range d.Args {
dArg := "&" + arg.VarName dArg := arg.VarName
if !arg.TypeReference.IsPtr() { if arg.Value == nil && arg.Default == nil {
if arg.Value != nil {
dArg = templates.Dump(arg.Value)
} else {
dArg = templates.Dump(arg.Default)
}
} else if arg.Value == nil && arg.Default == nil {
dArg = "nil" dArg = "nil"
} }
@@ -144,7 +167,7 @@ func (d *Directive) Declaration() string {
res := ucFirst(d.Name) + " func(ctx context.Context, obj interface{}, next graphql.Resolver" res := ucFirst(d.Name) + " func(ctx context.Context, obj interface{}, next graphql.Resolver"
for _, arg := range d.Args { for _, arg := range d.Args {
res += fmt.Sprintf(", %s %s", arg.Name, templates.CurrentImports.LookupType(arg.TypeReference.GO)) res += fmt.Sprintf(", %s %s", templates.ToGoPrivate(arg.Name), templates.CurrentImports.LookupType(arg.TypeReference.GO))
} }
res += ") (res interface{}, err error)" res += ") (res interface{}, err error)"

View File

@@ -0,0 +1,149 @@
{{ define "implDirectives" }}{{ $in := .DirectiveObjName }}
{{- range $i, $directive := .ImplDirectives -}}
directive{{add $i 1}} := func(ctx context.Context) (interface{}, error) {
{{- range $arg := $directive.Args }}
{{- if notNil "Value" $arg }}
{{ $arg.VarName }}, err := ec.{{ $arg.TypeReference.UnmarshalFunc }}(ctx, {{ $arg.Value | dump }})
if err != nil{
return nil, err
}
{{- else if notNil "Default" $arg }}
{{ $arg.VarName }}, err := ec.{{ $arg.TypeReference.UnmarshalFunc }}(ctx, {{ $arg.Default | dump }})
if err != nil{
return nil, err
}
{{- end }}
{{- end }}
if ec.directives.{{$directive.Name|ucFirst}} == nil {
return nil, errors.New("directive {{$directive.Name}} is not implemented")
}
return ec.directives.{{$directive.Name|ucFirst}}({{$directive.ResolveArgs $in $i }})
}
{{ end -}}
{{ end }}
{{define "queryDirectives"}}
for _, d := range obj.Directives {
switch d.Name {
{{- range $directive := . }}
case "{{$directive.Name}}":
{{- if $directive.Args }}
rawArgs := d.ArgumentMap(ec.Variables)
args, err := ec.{{ $directive.ArgsFunc }}(ctx,rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
{{- end }}
n := next
next = func(ctx context.Context) (interface{}, error) {
if ec.directives.{{$directive.Name|ucFirst}} == nil {
return nil, errors.New("directive {{$directive.Name}} is not implemented")
}
return ec.directives.{{$directive.Name|ucFirst}}({{$directive.CallArgs}})
}
{{- end }}
}
}
tmp, err := next(ctx)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if data, ok := tmp.(graphql.Marshaler); ok {
return data
}
ec.Errorf(ctx, `unexpected type %T from directive, should be graphql.Marshaler`, tmp)
return graphql.Null
{{end}}
{{ if .Directives.LocationDirectives "QUERY" }}
func (ec *executionContext) _queryMiddleware(ctx context.Context, obj *ast.OperationDefinition, next func(ctx context.Context) (interface{}, error)) graphql.Marshaler {
{{ template "queryDirectives" .Directives.LocationDirectives "QUERY" }}
}
{{ end }}
{{ if .Directives.LocationDirectives "MUTATION" }}
func (ec *executionContext) _mutationMiddleware(ctx context.Context, obj *ast.OperationDefinition, next func(ctx context.Context) (interface{}, error)) graphql.Marshaler {
{{ template "queryDirectives" .Directives.LocationDirectives "MUTATION" }}
}
{{ end }}
{{ if .Directives.LocationDirectives "SUBSCRIPTION" }}
func (ec *executionContext) _subscriptionMiddleware(ctx context.Context, obj *ast.OperationDefinition, next func(ctx context.Context) (interface{}, error)) func() graphql.Marshaler {
for _, d := range obj.Directives {
switch d.Name {
{{- range $directive := .Directives.LocationDirectives "SUBSCRIPTION" }}
case "{{$directive.Name}}":
{{- if $directive.Args }}
rawArgs := d.ArgumentMap(ec.Variables)
args, err := ec.{{ $directive.ArgsFunc }}(ctx,rawArgs)
if err != nil {
ec.Error(ctx, err)
return func() graphql.Marshaler {
return graphql.Null
}
}
{{- end }}
n := next
next = func(ctx context.Context) (interface{}, error) {
if ec.directives.{{$directive.Name|ucFirst}} == nil {
return nil, errors.New("directive {{$directive.Name}} is not implemented")
}
return ec.directives.{{$directive.Name|ucFirst}}({{$directive.CallArgs}})
}
{{- end }}
}
}
tmp, err := next(ctx)
if err != nil {
ec.Error(ctx, err)
return func() graphql.Marshaler {
return graphql.Null
}
}
if data, ok := tmp.(func() graphql.Marshaler); ok {
return data
}
ec.Errorf(ctx, `unexpected type %T from directive, should be graphql.Marshaler`, tmp)
return func() graphql.Marshaler {
return graphql.Null
}
}
{{ end }}
{{ if .Directives.LocationDirectives "FIELD" }}
func (ec *executionContext) _fieldMiddleware(ctx context.Context, obj interface{}, next graphql.Resolver) interface{} {
{{- if .Directives.LocationDirectives "FIELD" }}
fc := graphql.GetFieldContext(ctx)
for _, d := range fc.Field.Directives {
switch d.Name {
{{- range $directive := .Directives.LocationDirectives "FIELD" }}
case "{{$directive.Name}}":
{{- if $directive.Args }}
rawArgs := d.ArgumentMap(ec.Variables)
args, err := ec.{{ $directive.ArgsFunc }}(ctx,rawArgs)
if err != nil {
ec.Error(ctx, err)
return nil
}
{{- end }}
n := next
next = func(ctx context.Context) (interface{}, error) {
if ec.directives.{{$directive.Name|ucFirst}} == nil {
return nil, errors.New("directive {{$directive.Name}} is not implemented")
}
return ec.directives.{{$directive.Name|ucFirst}}({{$directive.CallArgs}})
}
{{- end }}
}
}
{{- end }}
res, err := ec.ResolverMiddleware(ctx, next)
if err != nil {
ec.Error(ctx, err)
return nil
}
return res
}
{{ end }}

View File

@@ -11,7 +11,7 @@ import (
"github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/codegen/templates" "github.com/99designs/gqlgen/codegen/templates"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
) )
type Field struct { type Field struct {
@@ -27,6 +27,7 @@ type Field struct {
NoErr bool // If this is bound to a go method, does that method have an error as the second argument NoErr bool // If this is bound to a go method, does that method have an error as the second argument
Object *Object // A link back to the parent object Object *Object // A link back to the parent object
Default interface{} // The default value Default interface{} // The default value
Stream bool // does this field return a channel?
Directives []*Directive Directives []*Directive
} }
@@ -73,17 +74,26 @@ func (b *builder) buildField(obj *Object, field *ast.FieldDefinition) (*Field, e
return &f, nil return &f, nil
} }
func (b *builder) bindField(obj *Object, f *Field) error { func (b *builder) bindField(obj *Object, f *Field) (errret error) {
defer func() { defer func() {
if f.TypeReference == nil { if f.TypeReference == nil {
tr, err := b.Binder.TypeReference(f.Type, nil) tr, err := b.Binder.TypeReference(f.Type, nil)
if err != nil { if err != nil {
panic(err) errret = err
} }
f.TypeReference = tr f.TypeReference = tr
} }
if f.TypeReference != nil {
dirs, err := b.getDirectives(f.TypeReference.Definition.Directives)
if err != nil {
errret = err
}
f.Directives = append(dirs, f.Directives...)
}
}() }()
f.Stream = obj.Stream
switch { switch {
case f.Name == "__schema": case f.Name == "__schema":
f.GoFieldType = GoFieldMethod f.GoFieldType = GoFieldMethod
@@ -95,6 +105,18 @@ func (b *builder) bindField(obj *Object, f *Field) error {
f.GoReceiverName = "ec" f.GoReceiverName = "ec"
f.GoFieldName = "introspectType" f.GoFieldName = "introspectType"
return nil return nil
case f.Name == "_entities":
f.GoFieldType = GoFieldMethod
f.GoReceiverName = "ec"
f.GoFieldName = "__resolve_entities"
f.MethodHasContext = true
return nil
case f.Name == "_service":
f.GoFieldType = GoFieldMethod
f.GoReceiverName = "ec"
f.GoFieldName = "__resolve__service"
f.MethodHasContext = true
return nil
case obj.Root: case obj.Root:
f.IsResolver = true f.IsResolver = true
return nil return nil
@@ -181,75 +203,155 @@ func (b *builder) bindField(obj *Object, f *Field) error {
} }
} }
// findField attempts to match the name to a struct field with the following // findBindTarget attempts to match the name to a field or method on a Type
// priorites: // with the following priorites:
// 1. Any method with a matching name // 1. Any Fields with a struct tag (see config.StructTag). Errors if more than one match is found
// 2. Any Fields with a struct tag (see config.StructTag) // 2. Any method or field with a matching name. Errors if more than one match is found
// 3. Any fields with a matching name // 3. Same logic again for embedded fields
// 4. Same logic again for embedded fields func (b *builder) findBindTarget(t types.Type, name string) (types.Object, error) {
func (b *builder) findBindTarget(named *types.Named, name string) (types.Object, error) { // NOTE: a struct tag will override both methods and fields
for i := 0; i < named.NumMethods(); i++ { // Bind to struct tag
method := named.Method(i) found, err := b.findBindStructTagTarget(t, name)
if !method.Exported() { if found != nil || err != nil {
continue return found, err
}
if !strings.EqualFold(method.Name(), name) {
continue
}
return method, nil
} }
strukt, ok := named.Underlying().(*types.Struct) // Search for a method to bind to
if !ok { foundMethod, err := b.findBindMethodTarget(t, name)
return nil, fmt.Errorf("not a struct") if err != nil {
return nil, err
} }
return b.findBindStructTarget(strukt, name)
// Search for a field to bind to
foundField, err := b.findBindFieldTarget(t, name)
if err != nil {
return nil, err
}
switch {
case foundField == nil && foundMethod != nil:
// Bind to method
return foundMethod, nil
case foundField != nil && foundMethod == nil:
// Bind to field
return foundField, nil
case foundField != nil && foundMethod != nil:
// Error
return nil, errors.Errorf("found more than one way to bind for %s", name)
}
// Search embeds
return b.findBindEmbedsTarget(t, name)
} }
func (b *builder) findBindStructTarget(strukt *types.Struct, name string) (types.Object, error) { func (b *builder) findBindStructTagTarget(in types.Type, name string) (types.Object, error) {
// struct tags have the highest priority if b.Config.StructTag == "" {
if b.Config.StructTag != "" { return nil, nil
var foundField *types.Var }
for i := 0; i < strukt.NumFields(); i++ {
field := strukt.Field(i) switch t := in.(type) {
if !field.Exported() { case *types.Named:
return b.findBindStructTagTarget(t.Underlying(), name)
case *types.Struct:
var found types.Object
for i := 0; i < t.NumFields(); i++ {
field := t.Field(i)
if !field.Exported() || field.Embedded() {
continue continue
} }
tags := reflect.StructTag(strukt.Tag(i)) tags := reflect.StructTag(t.Tag(i))
if val, ok := tags.Lookup(b.Config.StructTag); ok && equalFieldName(val, name) { if val, ok := tags.Lookup(b.Config.StructTag); ok && equalFieldName(val, name) {
if foundField != nil { if found != nil {
return nil, errors.Errorf("tag %s is ambigious; multiple fields have the same tag value of %s", b.Config.StructTag, val) return nil, errors.Errorf("tag %s is ambigious; multiple fields have the same tag value of %s", b.Config.StructTag, val)
} }
foundField = field found = field
} }
} }
if foundField != nil {
return foundField, nil return found, nil
}
} }
// Then matching field names return nil, nil
for i := 0; i < strukt.NumFields(); i++ { }
field := strukt.Field(i)
if !field.Exported() { func (b *builder) findBindMethodTarget(in types.Type, name string) (types.Object, error) {
continue switch t := in.(type) {
} case *types.Named:
if equalFieldName(field.Name(), name) { // aqui! if _, ok := t.Underlying().(*types.Interface); ok {
return field, nil return b.findBindMethodTarget(t.Underlying(), name)
} }
return b.findBindMethoderTarget(t.Method, t.NumMethods(), name)
case *types.Interface:
// FIX-ME: Should use ExplicitMethod here? What's the difference?
return b.findBindMethoderTarget(t.Method, t.NumMethods(), name)
} }
// Then look in embedded structs return nil, nil
for i := 0; i < strukt.NumFields(); i++ { }
field := strukt.Field(i)
if !field.Exported() { func (b *builder) findBindMethoderTarget(methodFunc func(i int) *types.Func, methodCount int, name string) (types.Object, error) {
var found types.Object
for i := 0; i < methodCount; i++ {
method := methodFunc(i)
if !method.Exported() || !strings.EqualFold(method.Name(), name) {
continue continue
} }
if !field.Anonymous() { if found != nil {
return nil, errors.Errorf("found more than one matching method to bind for %s", name)
}
found = method
}
return found, nil
}
func (b *builder) findBindFieldTarget(in types.Type, name string) (types.Object, error) {
switch t := in.(type) {
case *types.Named:
return b.findBindFieldTarget(t.Underlying(), name)
case *types.Struct:
var found types.Object
for i := 0; i < t.NumFields(); i++ {
field := t.Field(i)
if !field.Exported() || !equalFieldName(field.Name(), name) {
continue
}
if found != nil {
return nil, errors.Errorf("found more than one matching field to bind for %s", name)
}
found = field
}
return found, nil
}
return nil, nil
}
func (b *builder) findBindEmbedsTarget(in types.Type, name string) (types.Object, error) {
switch t := in.(type) {
case *types.Named:
return b.findBindEmbedsTarget(t.Underlying(), name)
case *types.Struct:
return b.findBindStructEmbedsTarget(t, name)
case *types.Interface:
return b.findBindInterfaceEmbedsTarget(t, name)
}
return nil, nil
}
func (b *builder) findBindStructEmbedsTarget(strukt *types.Struct, name string) (types.Object, error) {
var found types.Object
for i := 0; i < strukt.NumFields(); i++ {
field := strukt.Field(i)
if !field.Embedded() {
continue continue
} }
@@ -258,33 +360,68 @@ func (b *builder) findBindStructTarget(strukt *types.Struct, name string) (types
fieldType = ptr.Elem() fieldType = ptr.Elem()
} }
switch fieldType := fieldType.(type) { f, err := b.findBindTarget(fieldType, name)
case *types.Named: if err != nil {
f, err := b.findBindTarget(fieldType, name) return nil, err
if err != nil { }
return nil, err
} if f != nil && found != nil {
if f != nil { return nil, errors.Errorf("found more than one way to bind for %s", name)
return f, nil }
}
case *types.Struct: if f != nil {
f, err := b.findBindStructTarget(fieldType, name) found = f
if err != nil {
return nil, err
}
if f != nil {
return f, nil
}
default:
panic(fmt.Errorf("unknown embedded field type %T", field.Type()))
} }
} }
return nil, nil return found, nil
}
func (b *builder) findBindInterfaceEmbedsTarget(iface *types.Interface, name string) (types.Object, error) {
var found types.Object
for i := 0; i < iface.NumEmbeddeds(); i++ {
embeddedType := iface.EmbeddedType(i)
f, err := b.findBindTarget(embeddedType, name)
if err != nil {
return nil, err
}
if f != nil && found != nil {
return nil, errors.Errorf("found more than one way to bind for %s", name)
}
if f != nil {
found = f
}
}
return found, nil
} }
func (f *Field) HasDirectives() bool { func (f *Field) HasDirectives() bool {
return len(f.Directives) > 0 return len(f.ImplDirectives()) > 0
}
func (f *Field) DirectiveObjName() string {
if f.Object.Root {
return "nil"
}
return f.GoReceiverName
}
func (f *Field) ImplDirectives() []*Directive {
var d []*Directive
loc := ast.LocationFieldDefinition
if f.Object.IsInputType() {
loc = ast.LocationInputFieldDefinition
}
for i := range f.Directives {
if !f.Directives[i].Builtin && f.Directives[i].IsLocation(loc, ast.LocationObject) {
d = append(d, f.Directives[i])
}
}
return d
} }
func (f *Field) IsReserved() bool { func (f *Field) IsReserved() bool {
@@ -338,7 +475,7 @@ func (f *Field) ShortResolverDeclaration() string {
res := "(ctx context.Context" res := "(ctx context.Context"
if !f.Object.Root { if !f.Object.Root {
res += fmt.Sprintf(", obj *%s", templates.CurrentImports.LookupType(f.Object.Type)) res += fmt.Sprintf(", obj %s", templates.CurrentImports.LookupType(f.Object.Reference()))
} }
for _, arg := range f.Args { for _, arg := range f.Args {
res += fmt.Sprintf(", %s %s", arg.VarName, templates.CurrentImports.LookupType(arg.TypeReference.GO)) res += fmt.Sprintf(", %s %s", arg.VarName, templates.CurrentImports.LookupType(arg.TypeReference.GO))
@@ -354,7 +491,7 @@ func (f *Field) ShortResolverDeclaration() string {
} }
func (f *Field) ComplexitySignature() string { func (f *Field) ComplexitySignature() string {
res := fmt.Sprintf("func(childComplexity int") res := "func(childComplexity int"
for _, arg := range f.Args { for _, arg := range f.Args {
res += fmt.Sprintf(", %s %s", arg.VarName, templates.CurrentImports.LookupType(arg.TypeReference.GO)) res += fmt.Sprintf(", %s %s", arg.VarName, templates.CurrentImports.LookupType(arg.TypeReference.GO))
} }
@@ -363,16 +500,16 @@ func (f *Field) ComplexitySignature() string {
} }
func (f *Field) ComplexityArgs() string { func (f *Field) ComplexityArgs() string {
var args []string args := make([]string, len(f.Args))
for _, arg := range f.Args { for i, arg := range f.Args {
args = append(args, "args["+strconv.Quote(arg.Name)+"].("+templates.CurrentImports.LookupType(arg.TypeReference.GO)+")") args[i] = "args[" + strconv.Quote(arg.Name) + "].(" + templates.CurrentImports.LookupType(arg.TypeReference.GO) + ")"
} }
return strings.Join(args, ", ") return strings.Join(args, ", ")
} }
func (f *Field) CallArgs() string { func (f *Field) CallArgs() string {
var args []string args := make([]string, 0, len(f.Args)+2)
if f.IsResolver { if f.IsResolver {
args = append(args, "rctx") args = append(args, "rctx")
@@ -380,10 +517,8 @@ func (f *Field) CallArgs() string {
if !f.Object.Root { if !f.Object.Root {
args = append(args, "obj") args = append(args, "obj")
} }
} else { } else if f.MethodHasContext {
if f.MethodHasContext { args = append(args, "ctx")
args = append(args, "ctx")
}
} }
for _, arg := range f.Args { for _, arg := range f.Args {

View File

@@ -1,29 +1,57 @@
{{- range $object := .Objects }}{{- range $field := $object.Fields }} {{- range $object := .Objects }}{{- range $field := $object.Fields }}
{{- if $object.Stream }} func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Context, field graphql.CollectedField{{ if not $object.Root }}, obj {{$object.Reference | ref}}{{end}}) (ret {{ if $object.Stream }}func(){{ end }}graphql.Marshaler) {
func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Context, field graphql.CollectedField) func() graphql.Marshaler { {{- $null := "graphql.Null" }}
ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ {{- if $object.Stream }}
Field: field, {{- $null = "nil" }}
Args: nil, {{- end }}
}) defer func () {
{{- if $field.Args }} if r := recover(); r != nil {
rawArgs := field.ArgumentMap(ec.Variables) ec.Error(ctx, ec.Recover(ctx, r))
args, err := ec.{{ $field.ArgsFunc }}(ctx,rawArgs) ret = {{ $null }}
if err != nil { }
ec.Error(ctx, err) }()
return nil fc := &graphql.FieldContext{
} Object: {{$object.Name|quote}},
{{- end }} Field: field,
// FIXME: subscriptions are missing request middleware stack https://github.com/99designs/gqlgen/issues/259 Args: nil,
// and Tracer stack IsMethod: {{or $field.IsMethod $field.IsResolver}},
rctx := ctx }
results, err := ec.resolvers.{{ $field.ShortInvocation }}
ctx = graphql.WithFieldContext(ctx, fc)
{{- if $field.Args }}
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.{{ $field.ArgsFunc }}(ctx,rawArgs)
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
return nil return {{ $null }}
} }
fc.Args = args
{{- end }}
{{- if $.Directives.LocationDirectives "FIELD" }}
resTmp := ec._fieldMiddleware(ctx, {{if $object.Root}}nil{{else}}obj{{end}}, func(rctx context.Context) (interface{}, error) {
{{ template "field" $field }}
})
{{ else }}
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
{{ template "field" $field }}
})
if err != nil {
ec.Error(ctx, err)
return {{ $null }}
}
{{- end }}
if resTmp == nil {
{{- if $field.TypeReference.GQL.NonNull }}
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
{{- end }}
return {{ $null }}
}
{{- if $object.Stream }}
return func() graphql.Marshaler { return func() graphql.Marshaler {
res, ok := <-results res, ok := <-resTmp.(<-chan {{$field.TypeReference.GO | ref}})
if !ok { if !ok {
return nil return nil
} }
@@ -35,66 +63,60 @@
w.Write([]byte{'}'}) w.Write([]byte{'}'})
}) })
} }
} {{- else }}
{{ else }}
func (ec *executionContext) _{{$object.Name}}_{{$field.Name}}(ctx context.Context, field graphql.CollectedField{{ if not $object.Root }}, obj {{$object.Reference | ref}}{{end}}) graphql.Marshaler {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func () { ec.Tracer.EndFieldExecution(ctx) }()
rctx := &graphql.ResolverContext{
Object: {{$object.Name|quote}},
Field: field,
Args: nil,
IsMethod: {{or $field.IsMethod $field.IsResolver}},
}
ctx = graphql.WithResolverContext(ctx, rctx)
{{- if $field.Args }}
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.{{ $field.ArgsFunc }}(ctx,rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
rctx.Args = args
{{- end }}
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp := ec.FieldMiddleware(ctx, {{if $object.Root}}nil{{else}}obj{{end}}, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
{{- if $field.IsResolver }}
return ec.resolvers.{{ $field.ShortInvocation }}
{{- else if $field.IsMap }}
switch v := {{$field.GoReceiverName}}[{{$field.Name|quote}}].(type) {
case {{$field.TypeReference.GO | ref}}:
return v, nil
case {{$field.TypeReference.Elem.GO | ref}}:
return &v, nil
case nil:
return ({{$field.TypeReference.GO | ref}})(nil), nil
default:
return nil, fmt.Errorf("unexpected type %T for field %s", v, {{ $field.Name | quote}})
}
{{- else if $field.IsMethod }}
{{- if $field.NoErr }}
return {{$field.GoReceiverName}}.{{$field.GoFieldName}}({{ $field.CallArgs }}), nil
{{- else }}
return {{$field.GoReceiverName}}.{{$field.GoFieldName}}({{ $field.CallArgs }})
{{- end }}
{{- else if $field.IsVariable }}
return {{$field.GoReceiverName}}.{{$field.GoFieldName}}, nil
{{- end }}
})
if resTmp == nil {
{{- if $field.TypeReference.GQL.NonNull }}
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
{{- end }}
return graphql.Null
}
res := resTmp.({{$field.TypeReference.GO | ref}}) res := resTmp.({{$field.TypeReference.GO | ref}})
rctx.Result = res fc.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.{{ $field.TypeReference.MarshalFunc }}(ctx, field.Selections, res) return ec.{{ $field.TypeReference.MarshalFunc }}(ctx, field.Selections, res)
} {{- end }}
{{ end }} }
{{- end }}{{- end}} {{- end }}{{- end}}
{{ define "field" }}
{{- if .HasDirectives -}}
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
{{ template "fieldDefinition" . }}
}
{{ template "implDirectives" . }}
tmp, err := directive{{.ImplDirectives|len}}(rctx)
if err != nil {
return nil, err
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.({{if .Stream}}<-chan {{end}}{{ .TypeReference.GO | ref }}) ; ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be {{if .Stream}}<-chan {{end}}{{ .TypeReference.GO }}`, tmp)
{{- else -}}
ctx = rctx // use context from middleware stack in children
{{ template "fieldDefinition" . }}
{{- end -}}
{{ end }}
{{ define "fieldDefinition" }}
{{- if .IsResolver -}}
return ec.resolvers.{{ .ShortInvocation }}
{{- else if .IsMap -}}
switch v := {{.GoReceiverName}}[{{.Name|quote}}].(type) {
case {{if .Stream}}<-chan {{end}}{{.TypeReference.GO | ref}}:
return v, nil
case {{if .Stream}}<-chan {{end}}{{.TypeReference.Elem.GO | ref}}:
return &v, nil
case nil:
return ({{.TypeReference.GO | ref}})(nil), nil
default:
return nil, fmt.Errorf("unexpected type %T for field %s", v, {{ .Name | quote}})
}
{{- else if .IsMethod -}}
{{- if .NoErr -}}
return {{.GoReceiverName}}.{{.GoFieldName}}({{ .CallArgs }}), nil
{{- else -}}
return {{.GoReceiverName}}.{{.GoFieldName}}({{ .CallArgs }})
{{- end -}}
{{- else if .IsVariable -}}
return {{.GoReceiverName}}.{{.GoFieldName}}, nil
{{- end }}
{{- end }}

View File

@@ -11,5 +11,6 @@ func GenerateCode(data *Data) error {
Data: data, Data: data,
RegionTags: true, RegionTags: true,
GeneratedHeader: true, GeneratedHeader: true,
Packages: data.Config.Packages,
}) })
} }

View File

@@ -8,8 +8,8 @@
{{ reserveImport "errors" }} {{ reserveImport "errors" }}
{{ reserveImport "bytes" }} {{ reserveImport "bytes" }}
{{ reserveImport "github.com/vektah/gqlparser" }} {{ reserveImport "github.com/vektah/gqlparser/v2" "gqlparser" }}
{{ reserveImport "github.com/vektah/gqlparser/ast" }} {{ reserveImport "github.com/vektah/gqlparser/v2/ast" }}
{{ reserveImport "github.com/99designs/gqlgen/graphql" }} {{ reserveImport "github.com/99designs/gqlgen/graphql" }}
{{ reserveImport "github.com/99designs/gqlgen/graphql/introspection" }} {{ reserveImport "github.com/99designs/gqlgen/graphql/introspection" }}
@@ -39,7 +39,7 @@ type ResolverRoot interface {
type DirectiveRoot struct { type DirectiveRoot struct {
{{ range $directive := .Directives }} {{ range $directive := .Directives }}
{{ $directive.Declaration }} {{- $directive.Declaration }}
{{ end }} {{ end }}
} }
@@ -112,129 +112,86 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false return 0, false
} }
func (e *executableSchema) Query(ctx context.Context, op *ast.OperationDefinition) *graphql.Response { func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
{{- if .QueryRoot }} rc := graphql.GetOperationContext(ctx)
ec := executionContext{graphql.GetRequestContext(ctx), e} ec := executionContext{rc, e}
first := true
buf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte { switch rc.Operation.Operation {
data := ec._{{.QueryRoot.Name}}(ctx, op.SelectionSet) {{- if .QueryRoot }} case ast.Query:
return func(ctx context.Context) *graphql.Response {
if !first { return nil }
first = false
{{ if .Directives.LocationDirectives "QUERY" -}}
data := ec._queryMiddleware(ctx, rc.Operation, func(ctx context.Context) (interface{}, error){
return ec._{{.QueryRoot.Name}}(ctx, rc.Operation.SelectionSet), nil
})
{{- else -}}
data := ec._{{.QueryRoot.Name}}(ctx, rc.Operation.SelectionSet)
{{- end }}
var buf bytes.Buffer var buf bytes.Buffer
data.MarshalGQL(&buf) data.MarshalGQL(&buf)
return buf.Bytes()
})
return &graphql.Response{
Data: buf,
Errors: ec.Errors,
Extensions: ec.Extensions,
}
{{- else }}
return graphql.ErrorResponse(ctx, "queries are not supported")
{{- end }}
}
func (e *executableSchema) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
{{- if .MutationRoot }}
ec := executionContext{graphql.GetRequestContext(ctx), e}
buf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {
data := ec._{{.MutationRoot.Name}}(ctx, op.SelectionSet)
var buf bytes.Buffer
data.MarshalGQL(&buf)
return buf.Bytes()
})
return &graphql.Response{
Data: buf,
Errors: ec.Errors,
Extensions: ec.Extensions,
}
{{- else }}
return graphql.ErrorResponse(ctx, "mutations are not supported")
{{- end }}
}
func (e *executableSchema) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response {
{{- if .SubscriptionRoot }}
ec := executionContext{graphql.GetRequestContext(ctx), e}
next := ec._{{.SubscriptionRoot.Name}}(ctx, op.SelectionSet)
if ec.Errors != nil {
return graphql.OneShot(&graphql.Response{Data: []byte("null"), Errors: ec.Errors})
}
var buf bytes.Buffer
return func() *graphql.Response {
buf := ec.RequestMiddleware(ctx, func(ctx context.Context) []byte {
buf.Reset()
data := next()
if data == nil {
return nil
}
data.MarshalGQL(&buf)
return buf.Bytes()
})
if buf == nil {
return nil
}
return &graphql.Response{ return &graphql.Response{
Data: buf, Data: buf.Bytes(),
Errors: ec.Errors,
Extensions: ec.Extensions,
} }
} }
{{- else }} {{ end }}
return graphql.OneShot(graphql.ErrorResponse(ctx, "subscriptions are not supported"))
{{- end }} {{- if .MutationRoot }} case ast.Mutation:
return func(ctx context.Context) *graphql.Response {
if !first { return nil }
first = false
{{ if .Directives.LocationDirectives "MUTATION" -}}
data := ec._mutationMiddleware(ctx, rc.Operation, func(ctx context.Context) (interface{}, error){
return ec._{{.MutationRoot.Name}}(ctx, rc.Operation.SelectionSet), nil
})
{{- else -}}
data := ec._{{.MutationRoot.Name}}(ctx, rc.Operation.SelectionSet)
{{- end }}
var buf bytes.Buffer
data.MarshalGQL(&buf)
return &graphql.Response{
Data: buf.Bytes(),
}
}
{{ end }}
{{- if .SubscriptionRoot }} case ast.Subscription:
{{ if .Directives.LocationDirectives "SUBSCRIPTION" -}}
next := ec._subscriptionMiddleware(ctx, rc.Operation, func(ctx context.Context) (interface{}, error){
return ec._{{.SubscriptionRoot.Name}}(ctx, rc.Operation.SelectionSet),nil
})
{{- else -}}
next := ec._{{.SubscriptionRoot.Name}}(ctx, rc.Operation.SelectionSet)
{{- end }}
var buf bytes.Buffer
return func(ctx context.Context) *graphql.Response {
buf.Reset()
data := next()
if data == nil {
return nil
}
data.MarshalGQL(&buf)
return &graphql.Response{
Data: buf.Bytes(),
}
}
{{ end }}
default:
return graphql.OneShot(graphql.ErrorResponse(ctx, "unsupported GraphQL operation"))
}
} }
type executionContext struct { type executionContext struct {
*graphql.RequestContext *graphql.OperationContext
*executableSchema *executableSchema
} }
func (ec *executionContext) FieldMiddleware(ctx context.Context, obj interface{}, next graphql.Resolver) (ret interface{}) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
{{- if .Directives }}
rctx := graphql.GetResolverContext(ctx)
for _, d := range rctx.Field.Definition.Directives {
switch d.Name {
{{- range $directive := .Directives }}
case "{{$directive.Name}}":
if ec.directives.{{$directive.Name|ucFirst}} != nil {
{{- if $directive.Args }}
rawArgs := d.ArgumentMap(ec.Variables)
args, err := ec.{{ $directive.ArgsFunc }}(ctx,rawArgs)
if err != nil {
ec.Error(ctx, err)
return nil
}
{{- end }}
n := next
next = func(ctx context.Context) (interface{}, error) {
return ec.directives.{{$directive.Name|ucFirst}}({{$directive.CallArgs}})
}
}
{{- end }}
}
}
{{- end }}
res, err := ec.ResolverMiddleware(ctx, next)
if err != nil {
ec.Error(ctx, err)
return nil
}
return res
}
func (ec *executionContext) introspectSchema() (*introspection.Schema, error) { func (ec *executionContext) introspectSchema() (*introspection.Schema, error) {
if ec.DisableIntrospection { if ec.DisableIntrospection {
return nil, errors.New("introspection disabled") return nil, errors.New("introspection disabled")
@@ -249,8 +206,9 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil
} }
var parsedSchema = gqlparser.MustLoadSchema( var sources = []*ast.Source{
{{- range $filename, $schema := .SchemaStr }} {{- range $source := .Config.Sources }}
&ast.Source{Name: {{$filename|quote}}, Input: {{$schema|rawQuote}}}, {Name: {{$source.Name|quote}}, Input: {{$source.Input|rawQuote}}, BuiltIn: {{$source.BuiltIn}}},
{{- end }} {{- end }}
) }
var parsedSchema = gqlparser.MustLoadSchema(sources...)

View File

@@ -1,8 +1,8 @@
{{- range $input := .Inputs }} {{- range $input := .Inputs }}
{{- if not .HasUnmarshal }} {{- if not .HasUnmarshal }}
func (ec *executionContext) unmarshalInput{{ .Name }}(ctx context.Context, v interface{}) ({{.Type | ref}}, error) { func (ec *executionContext) unmarshalInput{{ .Name }}(ctx context.Context, obj interface{}) ({{.Type | ref}}, error) {
var it {{.Type | ref}} var it {{.Type | ref}}
var asMap = v.(map[string]interface{}) var asMap = obj.(map[string]interface{})
{{ range $field := .Fields}} {{ range $field := .Fields}}
{{- if $field.Default}} {{- if $field.Default}}
if _, present := asMap[{{$field.Name|quote}}] ; !present { if _, present := asMap[{{$field.Name|quote}}] ; !present {
@@ -16,27 +16,21 @@
{{- range $field := .Fields }} {{- range $field := .Fields }}
case {{$field.Name|quote}}: case {{$field.Name|quote}}:
var err error var err error
{{- if $field.Directives }}
getField0 := func(ctx context.Context) (interface{}, error) { return ec.{{ $field.TypeReference.UnmarshalFunc }}(ctx, v) }
{{- range $i, $directive := $field.Directives }} ctx := graphql.WithFieldInputContext(ctx, graphql.NewFieldInputWithField({{$field.Name|quote}}))
getField{{add $i 1}} := func(ctx context.Context) (res interface{}, err error) { {{- if $field.ImplDirectives }}
{{- range $dArg := $directive.Args }} directive0 := func(ctx context.Context) (interface{}, error) { return ec.{{ $field.TypeReference.UnmarshalFunc }}(ctx, v) }
{{- if and $dArg.TypeReference.IsPtr ( notNil "Value" $dArg ) }} {{ template "implDirectives" $field }}
{{ $dArg.VarName }} := {{ $dArg.Value | dump }} tmp, err := directive{{$field.ImplDirectives|len}}(ctx)
{{- end }}
{{- end }}
n := getField{{$i}}
return ec.directives.{{$directive.Name|ucFirst}}({{$directive.ResolveArgs "it" "n" }})
}
{{- end }}
tmp, err := getField{{$field.Directives|len}}(ctx)
if err != nil { if err != nil {
return it, err return it, err
} }
if data, ok := tmp.({{ $field.TypeReference.GO | ref }}) ; ok { if data, ok := tmp.({{ $field.TypeReference.GO | ref }}) ; ok {
it.{{$field.GoFieldName}} = data it.{{$field.GoFieldName}} = data
{{- if $field.TypeReference.IsNilable }}
} else if tmp == nil {
it.{{$field.GoFieldName}} = nil
{{- end }}
} else { } else {
return it, fmt.Errorf(`unexpected type %T from directive, should be {{ $field.TypeReference.GO }}`, tmp) return it, fmt.Errorf(`unexpected type %T from directive, should be {{ $field.TypeReference.GO }}`, tmp)
} }

View File

@@ -1,9 +1,13 @@
package codegen package codegen
import ( import (
"fmt"
"go/types" "go/types"
"github.com/vektah/gqlparser/ast" "github.com/pkg/errors"
"github.com/vektah/gqlparser/v2/ast"
"github.com/99designs/gqlgen/codegen/config"
) )
type Interface struct { type Interface struct {
@@ -16,11 +20,11 @@ type Interface struct {
type InterfaceImplementor struct { type InterfaceImplementor struct {
*ast.Definition *ast.Definition
Interface *Interface Type types.Type
Type types.Type TakeRef bool
} }
func (b *builder) buildInterface(typ *ast.Definition) *Interface { func (b *builder) buildInterface(typ *ast.Definition) (*Interface, error) {
obj, err := b.Binder.DefaultUserObject(typ.Name) obj, err := b.Binder.DefaultUserObject(typ.Name)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -32,32 +36,53 @@ func (b *builder) buildInterface(typ *ast.Definition) *Interface {
InTypemap: b.Config.Models.UserDefined(typ.Name), InTypemap: b.Config.Models.UserDefined(typ.Name),
} }
interfaceType, err := findGoInterface(i.Type)
if interfaceType == nil || err != nil {
return nil, fmt.Errorf("%s is not an interface", i.Type)
}
for _, implementor := range b.Schema.GetPossibleTypes(typ) { for _, implementor := range b.Schema.GetPossibleTypes(typ) {
obj, err := b.Binder.DefaultUserObject(implementor.Name) obj, err := b.Binder.DefaultUserObject(implementor.Name)
if err != nil { if err != nil {
panic(err) return nil, fmt.Errorf("%s has no backing go type", implementor.Name)
} }
i.Implementors = append(i.Implementors, InterfaceImplementor{ implementorType, err := findGoNamedType(obj)
Definition: implementor, if err != nil {
Type: obj, return nil, errors.Wrapf(err, "can not find backing go type %s", obj.String())
Interface: i, } else if implementorType == nil {
}) return nil, fmt.Errorf("can not find backing go type %s", obj.String())
}
anyValid := false
// first check if the value receiver can be nil, eg can we type switch on case Thing:
if types.Implements(implementorType, interfaceType) {
i.Implementors = append(i.Implementors, InterfaceImplementor{
Definition: implementor,
Type: obj,
TakeRef: !types.IsInterface(obj),
})
anyValid = true
}
// then check if the pointer receiver can be nil, eg can we type switch on case *Thing:
if types.Implements(types.NewPointer(implementorType), interfaceType) {
i.Implementors = append(i.Implementors, InterfaceImplementor{
Definition: implementor,
Type: types.NewPointer(obj),
})
anyValid = true
}
if !anyValid {
return nil, fmt.Errorf("%s does not satisfy the interface %s", implementorType.String(), i.Type.String())
}
} }
return i return i, nil
} }
func (i *InterfaceImplementor) ValueReceiver() bool { func (i *InterfaceImplementor) CanBeNil() bool {
interfaceType, err := findGoInterface(i.Interface.Type) return config.IsNilable(i.Type)
if interfaceType == nil || err != nil {
return true
}
implementorType, err := findGoNamedType(i.Type)
if implementorType == nil || err != nil {
return true
}
return types.Implements(implementorType, interfaceType)
} }

View File

@@ -1,16 +1,17 @@
{{- range $interface := .Interfaces }} {{- range $interface := .Interfaces }}
func (ec *executionContext) _{{$interface.Name}}(ctx context.Context, sel ast.SelectionSet, obj *{{$interface.Type | ref}}) graphql.Marshaler { func (ec *executionContext) _{{$interface.Name}}(ctx context.Context, sel ast.SelectionSet, obj {{$interface.Type | ref}}) graphql.Marshaler {
switch obj := (*obj).(type) { switch obj := (obj).(type) {
case nil: case nil:
return graphql.Null return graphql.Null
{{- range $implementor := $interface.Implementors }} {{- range $implementor := $interface.Implementors }}
{{- if $implementor.ValueReceiver }} case {{$implementor.Type | ref}}:
case {{$implementor.Type | ref}}: {{- if $implementor.CanBeNil }}
return ec._{{$implementor.Name}}(ctx, sel, &obj) if obj == nil {
{{- end}} return graphql.Null
case *{{$implementor.Type | ref}}: }
return ec._{{$implementor.Name}}(ctx, sel, obj) {{- end }}
return ec._{{$implementor.Name}}(ctx, sel, {{ if $implementor.TakeRef }}&{{ end }}obj)
{{- end }} {{- end }}
default: default:
panic(fmt.Errorf("unexpected type %T", obj)) panic(fmt.Errorf("unexpected type %T", obj))

View File

@@ -8,7 +8,7 @@ import (
"github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/config"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
) )
type GoFieldType int type GoFieldType int
@@ -82,11 +82,9 @@ func (b *builder) buildObject(typ *ast.Definition) (*Object, error) {
} }
func (o *Object) Reference() types.Type { func (o *Object) Reference() types.Type {
switch o.Type.(type) { if config.IsNilable(o.Type) {
case *types.Pointer, *types.Slice, *types.Map:
return o.Type return o.Type
} }
return types.NewPointer(o.Type) return types.NewPointer(o.Type)
} }
@@ -114,8 +112,7 @@ func (o *Object) HasUnmarshal() bool {
return true return true
} }
for i := 0; i < o.Type.(*types.Named).NumMethods(); i++ { for i := 0; i < o.Type.(*types.Named).NumMethods(); i++ {
switch o.Type.(*types.Named).Method(i).Name() { if o.Type.(*types.Named).Method(i).Name() == "UnmarshalGQL" {
case "UnmarshalGQL":
return true return true
} }
} }

View File

@@ -4,8 +4,8 @@ var {{ $object.Name|lcFirst}}Implementors = {{$object.Implementors}}
{{- if .Stream }} {{- if .Stream }}
func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.SelectionSet) func() graphql.Marshaler { func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.SelectionSet) func() graphql.Marshaler {
fields := graphql.CollectFields(ec.RequestContext, sel, {{$object.Name|lcFirst}}Implementors) fields := graphql.CollectFields(ec.OperationContext, sel, {{$object.Name|lcFirst}}Implementors)
ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{
Object: {{$object.Name|quote}}, Object: {{$object.Name|quote}},
}) })
if len(fields) != 1 { if len(fields) != 1 {
@@ -24,9 +24,9 @@ func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.Selec
} }
{{- else }} {{- else }}
func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.SelectionSet{{ if not $object.Root }},obj {{$object.Reference | ref }}{{ end }}) graphql.Marshaler { func (ec *executionContext) _{{$object.Name}}(ctx context.Context, sel ast.SelectionSet{{ if not $object.Root }},obj {{$object.Reference | ref }}{{ end }}) graphql.Marshaler {
fields := graphql.CollectFields(ec.RequestContext, sel, {{$object.Name|lcFirst}}Implementors) fields := graphql.CollectFields(ec.OperationContext, sel, {{$object.Name|lcFirst}}Implementors)
{{if $object.Root}} {{if $object.Root}}
ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{
Object: {{$object.Name|quote}}, Object: {{$object.Name|quote}},
}) })
{{end}} {{end}}

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"go/types" "go/types"
"strconv" "strconv"
"strings"
"github.com/99designs/gqlgen/internal/code" "github.com/99designs/gqlgen/internal/code"
) )
@@ -15,12 +16,13 @@ type Import struct {
} }
type Imports struct { type Imports struct {
imports []*Import imports []*Import
destDir string destDir string
packages *code.Packages
} }
func (i *Import) String() string { func (i *Import) String() string {
if i.Alias == i.Name { if strings.HasSuffix(i.Path, i.Alias) {
return strconv.Quote(i.Path) return strconv.Quote(i.Path)
} }
@@ -48,7 +50,7 @@ func (s *Imports) Reserve(path string, aliases ...string) (string, error) {
return "", nil return "", nil
} }
name := code.NameForPackage(path) name := s.packages.NameForPackage(path)
var alias string var alias string
if len(aliases) != 1 { if len(aliases) != 1 {
alias = name alias = name
@@ -93,7 +95,7 @@ func (s *Imports) Lookup(path string) string {
} }
imp := &Import{ imp := &Import{
Name: code.NameForPackage(path), Name: s.packages.NameForPackage(path),
Path: path, Path: path,
} }
s.imports = append(s.imports, imp) s.imports = append(s.imports, imp)

View File

@@ -15,6 +15,8 @@ import (
"text/template" "text/template"
"unicode" "unicode"
"github.com/99designs/gqlgen/internal/code"
"github.com/99designs/gqlgen/internal/imports" "github.com/99designs/gqlgen/internal/imports"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@@ -40,9 +42,16 @@ type Options struct {
Filename string Filename string
RegionTags bool RegionTags bool
GeneratedHeader bool GeneratedHeader bool
// PackageDoc is documentation written above the package line
PackageDoc string
// FileNotice is notice written below the package line
FileNotice string
// Data will be passed to the template execution. // Data will be passed to the template execution.
Data interface{} Data interface{}
Funcs template.FuncMap Funcs template.FuncMap
// Packages cache, you can find me on config.Config
Packages *code.Packages
} }
// Render renders a gql plugin template from the given Options. Render is an // Render renders a gql plugin template from the given Options. Render is an
@@ -53,7 +62,7 @@ func Render(cfg Options) error {
if CurrentImports != nil { if CurrentImports != nil {
panic(fmt.Errorf("recursive or concurrent call to RenderToFile detected")) panic(fmt.Errorf("recursive or concurrent call to RenderToFile detected"))
} }
CurrentImports = &Imports{destDir: filepath.Dir(cfg.Filename)} CurrentImports = &Imports{packages: cfg.Packages, destDir: filepath.Dir(cfg.Filename)}
// load path relative to calling source file // load path relative to calling source file
_, callerFile, _, _ := runtime.Caller(1) _, callerFile, _, _ := runtime.Caller(1)
@@ -131,9 +140,16 @@ func Render(cfg Options) error {
if cfg.GeneratedHeader { if cfg.GeneratedHeader {
result.WriteString("// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.\n\n") result.WriteString("// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.\n\n")
} }
if cfg.PackageDoc != "" {
result.WriteString(cfg.PackageDoc + "\n")
}
result.WriteString("package ") result.WriteString("package ")
result.WriteString(cfg.PackageName) result.WriteString(cfg.PackageName)
result.WriteString("\n\n") result.WriteString("\n\n")
if cfg.FileNotice != "" {
result.WriteString(cfg.FileNotice)
result.WriteString("\n\n")
}
result.WriteString("import (\n") result.WriteString("import (\n")
result.WriteString(CurrentImports.String()) result.WriteString(CurrentImports.String())
result.WriteString(")\n") result.WriteString(")\n")
@@ -143,7 +159,13 @@ func Render(cfg Options) error {
} }
CurrentImports = nil CurrentImports = nil
return write(cfg.Filename, result.Bytes()) err = write(cfg.Filename, result.Bytes(), cfg.Packages)
if err != nil {
return err
}
cfg.Packages.Evict(code.ImportPathForDir(filepath.Dir(cfg.Filename)))
return nil
} }
func center(width int, pad string, s string) string { func center(width int, pad string, s string) string {
@@ -157,8 +179,8 @@ func center(width int, pad string, s string) string {
func Funcs() template.FuncMap { func Funcs() template.FuncMap {
return template.FuncMap{ return template.FuncMap{
"ucFirst": ucFirst, "ucFirst": UcFirst,
"lcFirst": lcFirst, "lcFirst": LcFirst,
"quote": strconv.Quote, "quote": strconv.Quote,
"rawQuote": rawQuote, "rawQuote": rawQuote,
"dump": Dump, "dump": Dump,
@@ -180,7 +202,7 @@ func Funcs() template.FuncMap {
} }
} }
func ucFirst(s string) string { func UcFirst(s string) string {
if s == "" { if s == "" {
return "" return ""
} }
@@ -189,7 +211,7 @@ func ucFirst(s string) string {
return string(r) return string(r)
} }
func lcFirst(s string) string { func LcFirst(s string) string {
if s == "" { if s == "" {
return "" return ""
} }
@@ -211,6 +233,7 @@ var pkgReplacer = strings.NewReplacer(
"/", "ᚋ", "/", "ᚋ",
".", "ᚗ", ".", "ᚗ",
"-", "ᚑ", "-", "ᚑ",
"~", "א",
) )
func TypeIdentifier(t types.Type) string { func TypeIdentifier(t types.Type) string {
@@ -260,6 +283,9 @@ func Call(p *types.Func) string {
} }
func ToGo(name string) string { func ToGo(name string) string {
if name == "_" {
return "_"
}
runes := make([]rune, 0, len(name)) runes := make([]rune, 0, len(name))
wordWalker(name, func(info *wordInfo) { wordWalker(name, func(info *wordInfo) {
@@ -270,7 +296,7 @@ func ToGo(name string) string {
if strings.ToUpper(word) == word || strings.ToLower(word) == word { if strings.ToUpper(word) == word || strings.ToLower(word) == word {
// FOO or foo → Foo // FOO or foo → Foo
// FOo → FOo // FOo → FOo
word = ucFirst(strings.ToLower(word)) word = UcFirst(strings.ToLower(word))
} }
} }
runes = append(runes, []rune(word)...) runes = append(runes, []rune(word)...)
@@ -280,24 +306,28 @@ func ToGo(name string) string {
} }
func ToGoPrivate(name string) string { func ToGoPrivate(name string) string {
if name == "_" {
return "_"
}
runes := make([]rune, 0, len(name)) runes := make([]rune, 0, len(name))
first := true first := true
wordWalker(name, func(info *wordInfo) { wordWalker(name, func(info *wordInfo) {
word := info.Word word := info.Word
if first { switch {
case first:
if strings.ToUpper(word) == word || strings.ToLower(word) == word { if strings.ToUpper(word) == word || strings.ToLower(word) == word {
// ID → id, CAMEL → camel // ID → id, CAMEL → camel
word = strings.ToLower(info.Word) word = strings.ToLower(info.Word)
} else { } else {
// ITicket → iTicket // ITicket → iTicket
word = lcFirst(info.Word) word = LcFirst(info.Word)
} }
first = false first = false
} else if info.MatchCommonInitial { case info.MatchCommonInitial:
word = strings.ToUpper(word) word = strings.ToUpper(word)
} else if !info.HasCommonInitial { case !info.HasCommonInitial:
word = ucFirst(strings.ToLower(word)) word = UcFirst(strings.ToLower(word))
} }
runes = append(runes, []rune(word)...) runes = append(runes, []rune(word)...)
}) })
@@ -314,14 +344,15 @@ type wordInfo struct {
// This function is based on the following code. // This function is based on the following code.
// https://github.com/golang/lint/blob/06c8688daad7faa9da5a0c2f163a3d14aac986ca/lint.go#L679 // https://github.com/golang/lint/blob/06c8688daad7faa9da5a0c2f163a3d14aac986ca/lint.go#L679
func wordWalker(str string, f func(*wordInfo)) { func wordWalker(str string, f func(*wordInfo)) {
runes := []rune(str) runes := []rune(strings.TrimFunc(str, isDelimiter))
w, i := 0, 0 // index of start of word, scan w, i := 0, 0 // index of start of word, scan
hasCommonInitial := false hasCommonInitial := false
for i+1 <= len(runes) { for i+1 <= len(runes) {
eow := false // whether we hit the end of a word eow := false // whether we hit the end of a word
if i+1 == len(runes) { switch {
case i+1 == len(runes):
eow = true eow = true
} else if isDelimiter(runes[i+1]) { case isDelimiter(runes[i+1]):
// underscore; shift the remainder forward over any run of underscores // underscore; shift the remainder forward over any run of underscores
eow = true eow = true
n := 1 n := 1
@@ -336,7 +367,7 @@ func wordWalker(str string, f func(*wordInfo)) {
copy(runes[i+1:], runes[i+n+1:]) copy(runes[i+1:], runes[i+n+1:])
runes = runes[:len(runes)-n] runes = runes[:len(runes)-n]
} else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) { case unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]):
// lower->non-lower // lower->non-lower
eow = true eow = true
} }
@@ -429,6 +460,7 @@ var commonInitialisms = map[string]bool{
"IP": true, "IP": true,
"JSON": true, "JSON": true,
"LHS": true, "LHS": true,
"PGP": true,
"QPS": true, "QPS": true,
"RAM": true, "RAM": true,
"RHS": true, "RHS": true,
@@ -549,13 +581,13 @@ func render(filename string, tpldata interface{}) (*bytes.Buffer, error) {
return buf, t.Execute(buf, tpldata) return buf, t.Execute(buf, tpldata)
} }
func write(filename string, b []byte) error { func write(filename string, b []byte, packages *code.Packages) error {
err := os.MkdirAll(filepath.Dir(filename), 0755) err := os.MkdirAll(filepath.Dir(filename), 0755)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create directory") return errors.Wrap(err, "failed to create directory")
} }
formatted, err := imports.Prune(filename, b) formatted, err := imports.Prune(filename, b, packages)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "gofmt failed on %s: %s\n", filepath.Base(filename), err.Error()) fmt.Fprintf(os.Stderr, "gofmt failed on %s: %s\n", filepath.Base(filename), err.Error())
formatted = b formatted = b

View File

@@ -1,18 +1,32 @@
package codegen package codegen
import ( import (
"fmt"
"github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/config"
) )
func (b *builder) buildTypes() (map[string]*config.TypeReference, error) { func (b *builder) buildTypes() map[string]*config.TypeReference {
ret := map[string]*config.TypeReference{} ret := map[string]*config.TypeReference{}
for _, ref := range b.Binder.References { for _, ref := range b.Binder.References {
for ref != nil { processType(ret, ref)
ret[ref.UniquenessKey()] = ref }
return ret
}
ref = ref.Elem() func processType(ret map[string]*config.TypeReference, ref *config.TypeReference) {
key := ref.UniquenessKey()
if existing, found := ret[key]; found {
// Simplistic check of content which is obviously different.
existingGQL := fmt.Sprintf("%v", existing.GQL)
newGQL := fmt.Sprintf("%v", ref.GQL)
if existingGQL != newGQL {
panic(fmt.Sprintf("non-unique key \"%s\", trying to replace %s with %s", key, existingGQL, newGQL))
} }
} }
return ret, nil ret[key] = ref
if ref.IsSlice() {
processType(ret, ref.Elem())
}
} }

View File

@@ -1,13 +1,10 @@
{{- range $type := .ReferencedTypes }} {{- range $type := .ReferencedTypes }}
{{ with $type.UnmarshalFunc }} {{ with $type.UnmarshalFunc }}
func (ec *executionContext) {{ . }}(ctx context.Context, v interface{}) ({{ $type.GO | ref }}, error) { func (ec *executionContext) {{ . }}(ctx context.Context, v interface{}) ({{ $type.GO | ref }}, error) {
{{- if $type.IsNilable }} {{- if and $type.IsNilable (not $type.GQL.NonNull) }}
if v == nil { return nil, nil } if v == nil { return nil, nil }
{{- end }} {{- end }}
{{- if $type.IsPtr }} {{- if $type.IsSlice }}
res, err := ec.{{ $type.Elem.UnmarshalFunc }}(ctx, v)
return &res, err
{{- else if $type.IsSlice }}
var vSlice []interface{} var vSlice []interface{}
if v != nil { if v != nil {
if tmp1, ok := v.([]interface{}); ok { if tmp1, ok := v.([]interface{}); ok {
@@ -19,9 +16,10 @@
var err error var err error
res := make([]{{$type.GO.Elem | ref}}, len(vSlice)) res := make([]{{$type.GO.Elem | ref}}, len(vSlice))
for i := range vSlice { for i := range vSlice {
ctx := graphql.WithFieldInputContext(ctx, graphql.NewFieldInputWithIndex(i))
res[i], err = ec.{{ $type.Elem.UnmarshalFunc }}(ctx, vSlice[i]) res[i], err = ec.{{ $type.Elem.UnmarshalFunc }}(ctx, vSlice[i])
if err != nil { if err != nil {
return nil, err return nil, graphql.WrapErrorWithInputPath(ctx, err)
} }
} }
return res, nil return res, nil
@@ -29,17 +27,38 @@
{{- if $type.Unmarshaler }} {{- if $type.Unmarshaler }}
{{- if $type.CastType }} {{- if $type.CastType }}
tmp, err := {{ $type.Unmarshaler | call }}(v) tmp, err := {{ $type.Unmarshaler | call }}(v)
return {{ $type.GO | ref }}(tmp), err {{- if $type.IsNilable }}
res := {{ $type.Elem.GO | ref }}(tmp)
{{- else}}
res := {{ $type.GO | ref }}(tmp)
{{- end }}
{{- else}} {{- else}}
return {{ $type.Unmarshaler | call }}(v) res, err := {{ $type.Unmarshaler | call }}(v)
{{- end }}
{{- if and $type.IsTargetNilable (not $type.IsNilable) }}
return *res, graphql.WrapErrorWithInputPath(ctx, err)
{{- else if and (not $type.IsTargetNilable) $type.IsNilable }}
return &res, graphql.WrapErrorWithInputPath(ctx, err)
{{- else}}
return res, graphql.WrapErrorWithInputPath(ctx, err)
{{- end }} {{- end }}
{{- else if eq ($type.GO | ref) "map[string]interface{}" }} {{- else if eq ($type.GO | ref) "map[string]interface{}" }}
return v.(map[string]interface{}), nil return v.(map[string]interface{}), nil
{{- else if $type.IsMarshaler -}} {{- else if $type.IsMarshaler }}
var res {{ $type.GO | ref }} {{- if $type.IsNilable }}
return res, res.UnmarshalGQL(v) var res = new({{ $type.Elem.GO | ref }})
{{- else}}
var res {{ $type.GO | ref }}
{{- end }}
err := res.UnmarshalGQL(v)
return res, graphql.WrapErrorWithInputPath(ctx, err)
{{- else }} {{- else }}
return ec.unmarshalInput{{ $type.GQL.Name }}(ctx, v) res, err := ec.unmarshalInput{{ $type.GQL.Name }}(ctx, v)
{{- if $type.IsNilable }}
return &res, graphql.WrapErrorWithInputPath(ctx, err)
{{- else}}
return res, graphql.WrapErrorWithInputPath(ctx, err)
{{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
} }
@@ -47,17 +66,6 @@
{{ with $type.MarshalFunc }} {{ with $type.MarshalFunc }}
func (ec *executionContext) {{ . }}(ctx context.Context, sel ast.SelectionSet, v {{ $type.GO | ref }}) graphql.Marshaler { func (ec *executionContext) {{ . }}(ctx context.Context, sel ast.SelectionSet, v {{ $type.GO | ref }}) graphql.Marshaler {
{{- if $type.IsNilable }}
if v == nil {
{{- if $type.GQL.NonNull }}
if !ec.HasError(graphql.GetResolverContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
{{- end }}
return graphql.Null
}
{{- end }}
{{- if $type.IsSlice }} {{- if $type.IsSlice }}
{{- if not $type.GQL.NonNull }} {{- if not $type.GQL.NonNull }}
if v == nil { if v == nil {
@@ -75,11 +83,11 @@
for i := range v { for i := range v {
{{- if not $type.IsScalar }} {{- if not $type.IsScalar }}
i := i i := i
rctx := &graphql.ResolverContext{ fc := &graphql.FieldContext{
Index: &i, Index: &i,
Result: &v[i], Result: &v[i],
} }
ctx := graphql.WithResolverContext(ctx, rctx) ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) { f := func(i int) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -104,22 +112,35 @@
{{ if not $type.IsScalar }} wg.Wait() {{ end }} {{ if not $type.IsScalar }} wg.Wait() {{ end }}
return ret return ret
{{- else }} {{- else }}
{{- if $type.IsNilable }}
if v == nil {
{{- if $type.GQL.NonNull }}
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
{{- end }}
return graphql.Null
}
{{- end }}
{{- if $type.IsMarshaler }} {{- if $type.IsMarshaler }}
return v return v
{{- else if $type.Marshaler }} {{- else if $type.Marshaler }}
{{- if $type.IsPtr }} {{- $v := "v" }}
return ec.{{ $type.Elem.MarshalFunc }}(ctx, sel, *v) {{- if and $type.IsTargetNilable (not $type.IsNilable) }}
{{- else if $type.GQL.NonNull }} {{- $v = "&v" }}
res := {{ $type.Marshaler | call }}({{- if $type.CastType }}{{ $type.CastType | ref }}(v){{else}}v{{- end }}) {{- else if and (not $type.IsTargetNilable) $type.IsNilable }}
{{- $v = "*v" }}
{{- end }}
{{- if $type.GQL.NonNull }}
res := {{ $type.Marshaler | call }}({{- if $type.CastType }}{{ $type.CastType | ref }}({{ $v }}){{else}}{{ $v }}{{- end }})
if res == graphql.Null { if res == graphql.Null {
if !ec.HasError(graphql.GetResolverContext(ctx)) { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null") ec.Errorf(ctx, "must not be null")
} }
} }
return res return res
{{- else }} {{- else }}
return {{ $type.Marshaler | call }}({{- if $type.CastType }}{{ $type.CastType | ref }}(v){{else}}v{{- end }}) return {{ $type.Marshaler | call }}({{- if $type.CastType }}{{ $type.CastType | ref }}({{ $v }}){{else}}{{ $v }}{{- end }})
{{- end }} {{- end }}
{{- else }} {{- else }}
return ec._{{$type.Definition.Name}}(ctx, sel, {{ if not $type.IsNilable}}&{{end}} v) return ec._{{$type.Definition.Name}}(ctx, sel, {{ if not $type.IsNilable}}&{{end}} v)

View File

@@ -2,7 +2,7 @@ package complexity
import ( import (
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
) )
func Calculate(es graphql.ExecutableSchema, op *ast.OperationDefinition, vars map[string]interface{}) int { func Calculate(es graphql.ExecutableSchema, op *ast.OperationDefinition, vars map[string]interface{}) int {

View File

@@ -1,14 +1,19 @@
module github.com/99designs/gqlgen module github.com/99designs/gqlgen
go 1.12
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/agnivade/levenshtein v1.0.3 // indirect
github.com/go-chi/chi v3.3.2+incompatible github.com/go-chi/chi v3.3.2+incompatible
github.com/gogo/protobuf v1.0.0 // indirect github.com/gogo/protobuf v1.0.0 // indirect
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect
github.com/gorilla/mux v1.6.1 // indirect github.com/gorilla/mux v1.6.1 // indirect
github.com/gorilla/websocket v1.2.0 github.com/gorilla/websocket v1.4.2
github.com/hashicorp/golang-lru v0.5.0 github.com/hashicorp/golang-lru v0.5.0
github.com/kr/pretty v0.1.0 // indirect github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007
github.com/mattn/go-colorable v0.1.4
github.com/mattn/go-isatty v0.0.12
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047
github.com/opentracing/basictracer-go v1.0.0 // indirect github.com/opentracing/basictracer-go v1.0.0 // indirect
github.com/opentracing/opentracing-go v1.0.2 github.com/opentracing/opentracing-go v1.0.2
@@ -16,13 +21,13 @@ require (
github.com/rs/cors v1.6.0 github.com/rs/cors v1.6.0
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 // indirect github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 // indirect
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 // indirect github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 // indirect
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.4.0
github.com/urfave/cli v1.20.0 github.com/urfave/cli/v2 v2.1.1
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e
github.com/vektah/gqlparser v1.1.2 github.com/vektah/gqlparser v1.3.1
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd github.com/vektah/gqlparser/v2 v2.0.1
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.2.4
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755 sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67 // indirect sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67 // indirect
) )

View File

@@ -1,11 +1,20 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ= github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ= github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/gogo/protobuf v1.0.0 h1:2jyBKDKU/8v3v2xVR2PtiWQviFUyiaGk2rpfyFT8rTM= github.com/gogo/protobuf v1.0.0 h1:2jyBKDKU/8v3v2xVR2PtiWQviFUyiaGk2rpfyFT8rTM=
@@ -14,8 +23,8 @@ github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f h1:9oNbS1z4rVpbnkH
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.1 h1:KOwqsTYZdeuMacU7CxjMNYEKeBvLbxW+psodrbcEa3A= github.com/gorilla/mux v1.6.1 h1:KOwqsTYZdeuMacU7CxjMNYEKeBvLbxW+psodrbcEa3A=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -23,6 +32,15 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8= github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo=
@@ -35,46 +53,61 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 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/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371 h1:SWV2fHctRpRrp49VXJ6UZja7gU9QLHwRpIPBN89SKEo=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 h1:JJV9CsgM9EC9w2iVkwuz+sMx8yRFe89PJRUrv6hPCIA= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0 h1:JJV9CsgM9EC9w2iVkwuz+sMx8yRFe89PJRUrv6hPCIA=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U= github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/vektah/dataloaden v0.2.0 h1:lhynDrG7c8mNLahboCo0Wq82tMjmu5yOUv2ds/tBmss= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/vektah/dataloaden v0.2.0/go.mod h1:vxM6NuRlgiR0M6wbVTJeKp9vQIs81ZMfCYO+4yq/jbE= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68= github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74=
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180404174746-b3c676e531a6 h1:mge3qS/eMvcfyIAzTMOAy0XUzWG6Lk0N4M8zjuSmdco= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180404174746-b3c676e531a6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6 h1:iZgcI2DDp6zW5v9Z/5+f0NuqoxNdmzg4hivjk2WLXpY= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6 h1:iZgcI2DDp6zW5v9Z/5+f0NuqoxNdmzg4hivjk2WLXpY=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-dbeab5af4b8d3204d444b78cafaba18a9a062a50/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190511041617-99f201b6807e h1:wTxRxdzKt8fn3IQa3+kVlPJMxK2hJj2Orm+M2Mzw9eg=
golang.org/x/tools v0.0.0-20190511041617-99f201b6807e/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw= golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755 h1:d2maSb13hr/ArmfK3rW+wNUKKfytCol7W1/vDHxMPiE= sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755 h1:d2maSb13hr/ArmfK3rW+wNUKKfytCol7W1/vDHxMPiE=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67 h1:e1sMhtVq9AfcEy8AXNb8eSg6gbzfdpYhoNqnPJa+GzI= sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67 h1:e1sMhtVq9AfcEy8AXNb8eSg6gbzfdpYhoNqnPJa+GzI=

29
vendor/github.com/99designs/gqlgen/graphql/cache.go generated vendored Normal file
View File

@@ -0,0 +1,29 @@
package graphql
import "context"
// Cache is a shared store for APQ and query AST caching
type Cache interface {
// Get looks up a key's value from the cache.
Get(ctx context.Context, key string) (value interface{}, ok bool)
// Add adds a value to the cache.
Add(ctx context.Context, key string, value interface{})
}
// MapCache is the simplest implementation of a cache, because it can not evict it should only be used in tests
type MapCache map[string]interface{}
// Get looks up a key's value from the cache.
func (m MapCache) Get(ctx context.Context, key string) (value interface{}, ok bool) {
v, ok := m[key]
return v, ok
}
// Add adds a value to the cache.
func (m MapCache) Add(ctx context.Context, key string, value interface{}) { m[key] = value }
type NoCache struct{}
func (n NoCache) Get(ctx context.Context, key string) (value interface{}, ok bool) { return nil, false }
func (n NoCache) Add(ctx context.Context, key string, value interface{}) {}

View File

@@ -1,274 +0,0 @@
package graphql
import (
"context"
"fmt"
"sync"
"github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/gqlerror"
)
type Resolver func(ctx context.Context) (res interface{}, err error)
type FieldMiddleware func(ctx context.Context, next Resolver) (res interface{}, err error)
type RequestMiddleware func(ctx context.Context, next func(ctx context.Context) []byte) []byte
type ComplexityLimitFunc func(ctx context.Context) int
type RequestContext struct {
RawQuery string
Variables map[string]interface{}
Doc *ast.QueryDocument
ComplexityLimit int
OperationComplexity int
DisableIntrospection bool
// ErrorPresenter will be used to generate the error
// message from errors given to Error().
ErrorPresenter ErrorPresenterFunc
Recover RecoverFunc
ResolverMiddleware FieldMiddleware
DirectiveMiddleware FieldMiddleware
RequestMiddleware RequestMiddleware
Tracer Tracer
errorsMu sync.Mutex
Errors gqlerror.List
extensionsMu sync.Mutex
Extensions map[string]interface{}
}
func DefaultResolverMiddleware(ctx context.Context, next Resolver) (res interface{}, err error) {
return next(ctx)
}
func DefaultDirectiveMiddleware(ctx context.Context, next Resolver) (res interface{}, err error) {
return next(ctx)
}
func DefaultRequestMiddleware(ctx context.Context, next func(ctx context.Context) []byte) []byte {
return next(ctx)
}
func NewRequestContext(doc *ast.QueryDocument, query string, variables map[string]interface{}) *RequestContext {
return &RequestContext{
Doc: doc,
RawQuery: query,
Variables: variables,
ResolverMiddleware: DefaultResolverMiddleware,
DirectiveMiddleware: DefaultDirectiveMiddleware,
RequestMiddleware: DefaultRequestMiddleware,
Recover: DefaultRecover,
ErrorPresenter: DefaultErrorPresenter,
Tracer: &NopTracer{},
}
}
type key string
const (
request key = "request_context"
resolver key = "resolver_context"
)
func GetRequestContext(ctx context.Context) *RequestContext {
if val, ok := ctx.Value(request).(*RequestContext); ok {
return val
}
return nil
}
func WithRequestContext(ctx context.Context, rc *RequestContext) context.Context {
return context.WithValue(ctx, request, rc)
}
type ResolverContext struct {
Parent *ResolverContext
// The name of the type this field belongs to
Object string
// These are the args after processing, they can be mutated in middleware to change what the resolver will get.
Args map[string]interface{}
// The raw field
Field CollectedField
// The index of array in path.
Index *int
// The result object of resolver
Result interface{}
// IsMethod indicates if the resolver is a method
IsMethod bool
}
func (r *ResolverContext) Path() []interface{} {
var path []interface{}
for it := r; it != nil; it = it.Parent {
if it.Index != nil {
path = append(path, *it.Index)
} else if it.Field.Field != nil {
path = append(path, it.Field.Alias)
}
}
// because we are walking up the chain, all the elements are backwards, do an inplace flip.
for i := len(path)/2 - 1; i >= 0; i-- {
opp := len(path) - 1 - i
path[i], path[opp] = path[opp], path[i]
}
return path
}
func GetResolverContext(ctx context.Context) *ResolverContext {
if val, ok := ctx.Value(resolver).(*ResolverContext); ok {
return val
}
return nil
}
func WithResolverContext(ctx context.Context, rc *ResolverContext) context.Context {
rc.Parent = GetResolverContext(ctx)
return context.WithValue(ctx, resolver, rc)
}
// This is just a convenient wrapper method for CollectFields
func CollectFieldsCtx(ctx context.Context, satisfies []string) []CollectedField {
resctx := GetResolverContext(ctx)
return CollectFields(GetRequestContext(ctx), resctx.Field.Selections, satisfies)
}
// CollectAllFields returns a slice of all GraphQL field names that were selected for the current resolver context.
// The slice will contain the unique set of all field names requested regardless of fragment type conditions.
func CollectAllFields(ctx context.Context) []string {
resctx := GetResolverContext(ctx)
collected := CollectFields(GetRequestContext(ctx), resctx.Field.Selections, nil)
uniq := make([]string, 0, len(collected))
Next:
for _, f := range collected {
for _, name := range uniq {
if name == f.Name {
continue Next
}
}
uniq = append(uniq, f.Name)
}
return uniq
}
// Errorf sends an error string to the client, passing it through the formatter.
func (c *RequestContext) Errorf(ctx context.Context, format string, args ...interface{}) {
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
c.Errors = append(c.Errors, c.ErrorPresenter(ctx, fmt.Errorf(format, args...)))
}
// Error sends an error to the client, passing it through the formatter.
func (c *RequestContext) Error(ctx context.Context, err error) {
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
c.Errors = append(c.Errors, c.ErrorPresenter(ctx, err))
}
// HasError returns true if the current field has already errored
func (c *RequestContext) HasError(rctx *ResolverContext) bool {
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
path := rctx.Path()
for _, err := range c.Errors {
if equalPath(err.Path, path) {
return true
}
}
return false
}
// GetErrors returns a list of errors that occurred in the current field
func (c *RequestContext) GetErrors(rctx *ResolverContext) gqlerror.List {
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
path := rctx.Path()
var errs gqlerror.List
for _, err := range c.Errors {
if equalPath(err.Path, path) {
errs = append(errs, err)
}
}
return errs
}
func equalPath(a []interface{}, b []interface{}) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
// AddError is a convenience method for adding an error to the current response
func AddError(ctx context.Context, err error) {
GetRequestContext(ctx).Error(ctx, err)
}
// AddErrorf is a convenience method for adding an error to the current response
func AddErrorf(ctx context.Context, format string, args ...interface{}) {
GetRequestContext(ctx).Errorf(ctx, format, args...)
}
// RegisterExtension registers an extension, returns error if extension has already been registered
func (c *RequestContext) RegisterExtension(key string, value interface{}) error {
c.extensionsMu.Lock()
defer c.extensionsMu.Unlock()
if c.Extensions == nil {
c.Extensions = make(map[string]interface{})
}
if _, ok := c.Extensions[key]; ok {
return fmt.Errorf("extension already registered for key %s", key)
}
c.Extensions[key] = value
return nil
}
// ChainFieldMiddleware add chain by FieldMiddleware
func ChainFieldMiddleware(handleFunc ...FieldMiddleware) FieldMiddleware {
n := len(handleFunc)
if n > 1 {
lastI := n - 1
return func(ctx context.Context, next Resolver) (interface{}, error) {
var (
chainHandler Resolver
curI int
)
chainHandler = func(currentCtx context.Context) (interface{}, error) {
if curI == lastI {
return next(currentCtx)
}
curI++
res, err := handleFunc[curI](currentCtx, chainHandler)
curI--
return res, err
}
return handleFunc[0](ctx, chainHandler)
}
}
if n == 1 {
return handleFunc[0]
}
return func(ctx context.Context, next Resolver) (interface{}, error) {
return next(ctx)
}
}

View File

@@ -0,0 +1,92 @@
package graphql
import (
"context"
"time"
"github.com/vektah/gqlparser/v2/ast"
)
type key string
const resolverCtx key = "resolver_context"
// Deprecated: Use FieldContext instead
type ResolverContext = FieldContext
type FieldContext struct {
Parent *FieldContext
// The name of the type this field belongs to
Object string
// These are the args after processing, they can be mutated in middleware to change what the resolver will get.
Args map[string]interface{}
// The raw field
Field CollectedField
// The index of array in path.
Index *int
// The result object of resolver
Result interface{}
// IsMethod indicates if the resolver is a method
IsMethod bool
}
type FieldStats struct {
// When field execution started
Started time.Time
// When argument marshaling finished
ArgumentsCompleted time.Time
// When the field completed running all middleware. Not available inside field middleware!
Completed time.Time
}
func (r *FieldContext) Path() ast.Path {
var path ast.Path
for it := r; it != nil; it = it.Parent {
if it.Index != nil {
path = append(path, ast.PathIndex(*it.Index))
} else if it.Field.Field != nil {
path = append(path, ast.PathName(it.Field.Alias))
}
}
// because we are walking up the chain, all the elements are backwards, do an inplace flip.
for i := len(path)/2 - 1; i >= 0; i-- {
opp := len(path) - 1 - i
path[i], path[opp] = path[opp], path[i]
}
return path
}
// Deprecated: Use GetFieldContext instead
func GetResolverContext(ctx context.Context) *ResolverContext {
return GetFieldContext(ctx)
}
func GetFieldContext(ctx context.Context) *FieldContext {
if val, ok := ctx.Value(resolverCtx).(*FieldContext); ok {
return val
}
return nil
}
func WithFieldContext(ctx context.Context, rc *FieldContext) context.Context {
rc.Parent = GetFieldContext(ctx)
return context.WithValue(ctx, resolverCtx, rc)
}
func equalPath(a ast.Path, b ast.Path) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@@ -0,0 +1,85 @@
package graphql
import (
"context"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
const fieldInputCtx key = "field_input_context"
type FieldInputContext struct {
ParentField *FieldContext
ParentInput *FieldInputContext
Field *string
Index *int
}
func (fic *FieldInputContext) Path() ast.Path {
var inputPath ast.Path
for it := fic; it != nil; it = it.ParentInput {
if it.Index != nil {
inputPath = append(inputPath, ast.PathIndex(*it.Index))
} else if it.Field != nil {
inputPath = append(inputPath, ast.PathName(*it.Field))
}
}
// because we are walking up the chain, all the elements are backwards, do an inplace flip.
for i := len(inputPath)/2 - 1; i >= 0; i-- {
opp := len(inputPath) - 1 - i
inputPath[i], inputPath[opp] = inputPath[opp], inputPath[i]
}
if fic.ParentField != nil {
fieldPath := fic.ParentField.Path()
return append(fieldPath, inputPath...)
}
return inputPath
}
func NewFieldInputWithField(field string) *FieldInputContext {
return &FieldInputContext{Field: &field}
}
func NewFieldInputWithIndex(index int) *FieldInputContext {
return &FieldInputContext{Index: &index}
}
func WithFieldInputContext(ctx context.Context, fic *FieldInputContext) context.Context {
if fieldContext := GetFieldContext(ctx); fieldContext != nil {
fic.ParentField = fieldContext
}
if fieldInputContext := GetFieldInputContext(ctx); fieldInputContext != nil {
fic.ParentInput = fieldInputContext
}
return context.WithValue(ctx, fieldInputCtx, fic)
}
func GetFieldInputContext(ctx context.Context) *FieldInputContext {
if val, ok := ctx.Value(fieldInputCtx).(*FieldInputContext); ok {
return val
}
return nil
}
func WrapErrorWithInputPath(ctx context.Context, err error) error {
if err == nil {
return nil
}
inputContext := GetFieldInputContext(ctx)
path := inputContext.Path()
if gerr, ok := err.(*gqlerror.Error); ok {
if gerr.Path == nil {
gerr.Path = path
}
return gerr
} else {
return gqlerror.WrapPath(path, err)
}
}

View File

@@ -0,0 +1,107 @@
package graphql
import (
"context"
"errors"
"github.com/vektah/gqlparser/v2/ast"
)
// Deprecated: Please update all references to OperationContext instead
type RequestContext = OperationContext
type OperationContext struct {
RawQuery string
Variables map[string]interface{}
OperationName string
Doc *ast.QueryDocument
Operation *ast.OperationDefinition
DisableIntrospection bool
Recover RecoverFunc
ResolverMiddleware FieldMiddleware
Stats Stats
}
func (c *OperationContext) Validate(ctx context.Context) error {
if c.Doc == nil {
return errors.New("field 'Doc'is required")
}
if c.RawQuery == "" {
return errors.New("field 'RawQuery' is required")
}
if c.Variables == nil {
c.Variables = make(map[string]interface{})
}
if c.ResolverMiddleware == nil {
return errors.New("field 'ResolverMiddleware' is required")
}
if c.Recover == nil {
c.Recover = DefaultRecover
}
return nil
}
const operationCtx key = "operation_context"
// Deprecated: Please update all references to GetOperationContext instead
func GetRequestContext(ctx context.Context) *RequestContext {
return GetOperationContext(ctx)
}
func GetOperationContext(ctx context.Context) *OperationContext {
if val, ok := ctx.Value(operationCtx).(*OperationContext); ok && val != nil {
return val
}
panic("missing operation context")
}
func WithOperationContext(ctx context.Context, rc *OperationContext) context.Context {
return context.WithValue(ctx, operationCtx, rc)
}
// HasOperationContext checks if the given context is part of an ongoing operation
//
// Some errors can happen outside of an operation, eg json unmarshal errors.
func HasOperationContext(ctx context.Context) bool {
_, ok := ctx.Value(operationCtx).(*OperationContext)
return ok
}
// This is just a convenient wrapper method for CollectFields
func CollectFieldsCtx(ctx context.Context, satisfies []string) []CollectedField {
resctx := GetFieldContext(ctx)
return CollectFields(GetOperationContext(ctx), resctx.Field.Selections, satisfies)
}
// CollectAllFields returns a slice of all GraphQL field names that were selected for the current resolver context.
// The slice will contain the unique set of all field names requested regardless of fragment type conditions.
func CollectAllFields(ctx context.Context) []string {
resctx := GetFieldContext(ctx)
collected := CollectFields(GetOperationContext(ctx), resctx.Field.Selections, nil)
uniq := make([]string, 0, len(collected))
Next:
for _, f := range collected {
for _, name := range uniq {
if name == f.Name {
continue Next
}
}
uniq = append(uniq, f.Name)
}
return uniq
}
// Errorf sends an error string to the client, passing it through the formatter.
// Deprecated: use graphql.AddErrorf(ctx, err) instead
func (c *OperationContext) Errorf(ctx context.Context, format string, args ...interface{}) {
AddErrorf(ctx, format, args...)
}
// Error sends an error to the client, passing it through the formatter.
// Deprecated: use graphql.AddError(ctx, err) instead
func (c *OperationContext) Error(ctx context.Context, err error) {
AddError(ctx, err)
}

View File

@@ -0,0 +1,157 @@
package graphql
import (
"context"
"fmt"
"sync"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type responseContext struct {
errorPresenter ErrorPresenterFunc
recover RecoverFunc
errors gqlerror.List
errorsMu sync.Mutex
extensions map[string]interface{}
extensionsMu sync.Mutex
}
const resultCtx key = "result_context"
func getResponseContext(ctx context.Context) *responseContext {
val, ok := ctx.Value(resultCtx).(*responseContext)
if !ok {
panic("missing response context")
}
return val
}
func WithResponseContext(ctx context.Context, presenterFunc ErrorPresenterFunc, recoverFunc RecoverFunc) context.Context {
return context.WithValue(ctx, resultCtx, &responseContext{
errorPresenter: presenterFunc,
recover: recoverFunc,
})
}
// AddErrorf writes a formatted error to the client, first passing it through the error presenter.
func AddErrorf(ctx context.Context, format string, args ...interface{}) {
c := getResponseContext(ctx)
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
c.errors = append(c.errors, c.errorPresenter(ctx, fmt.Errorf(format, args...)))
}
// AddError sends an error to the client, first passing it through the error presenter.
func AddError(ctx context.Context, err error) {
c := getResponseContext(ctx)
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
c.errors = append(c.errors, c.errorPresenter(ctx, err))
}
func Recover(ctx context.Context, err interface{}) (userMessage error) {
c := getResponseContext(ctx)
return c.recover(ctx, err)
}
// HasFieldError returns true if the given field has already errored
func HasFieldError(ctx context.Context, rctx *FieldContext) bool {
c := getResponseContext(ctx)
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
if len(c.errors) == 0 {
return false
}
path := rctx.Path()
for _, err := range c.errors {
if equalPath(err.Path, path) {
return true
}
}
return false
}
// GetFieldErrors returns a list of errors that occurred in the given field
func GetFieldErrors(ctx context.Context, rctx *FieldContext) gqlerror.List {
c := getResponseContext(ctx)
c.errorsMu.Lock()
defer c.errorsMu.Unlock()
if len(c.errors) == 0 {
return nil
}
path := rctx.Path()
var errs gqlerror.List
for _, err := range c.errors {
if equalPath(err.Path, path) {
errs = append(errs, err)
}
}
return errs
}
func GetErrors(ctx context.Context) gqlerror.List {
resCtx := getResponseContext(ctx)
resCtx.errorsMu.Lock()
defer resCtx.errorsMu.Unlock()
if len(resCtx.errors) == 0 {
return nil
}
errs := resCtx.errors
cpy := make(gqlerror.List, len(errs))
for i := range errs {
errCpy := *errs[i]
cpy[i] = &errCpy
}
return cpy
}
// RegisterExtension allows you to add a new extension into the graphql response
func RegisterExtension(ctx context.Context, key string, value interface{}) {
c := getResponseContext(ctx)
c.extensionsMu.Lock()
defer c.extensionsMu.Unlock()
if c.extensions == nil {
c.extensions = make(map[string]interface{})
}
if _, ok := c.extensions[key]; ok {
panic(fmt.Errorf("extension already registered for key %s", key))
}
c.extensions[key] = value
}
// GetExtensions returns any extensions registered in the current result context
func GetExtensions(ctx context.Context) map[string]interface{} {
ext := getResponseContext(ctx).extensions
if ext == nil {
return map[string]interface{}{}
}
return ext
}
func GetExtension(ctx context.Context, name string) interface{} {
ext := getResponseContext(ctx).extensions
if ext == nil {
return nil
}
return ext[name]
}

View File

@@ -0,0 +1,49 @@
package errcode
import (
"github.com/vektah/gqlparser/v2/gqlerror"
)
const ValidationFailed = "GRAPHQL_VALIDATION_FAILED"
const ParseFailed = "GRAPHQL_PARSE_FAILED"
type ErrorKind int
const (
// issues with graphql (validation, parsing). 422s in http, GQL_ERROR in websocket
KindProtocol ErrorKind = iota
// user errors, 200s in http, GQL_DATA in websocket
KindUser
)
var codeType = map[string]ErrorKind{
ValidationFailed: KindProtocol,
ParseFailed: KindProtocol,
}
// RegisterErrorType should be called by extensions that want to customize the http status codes for errors they return
func RegisterErrorType(code string, kind ErrorKind) {
codeType[code] = kind
}
// Set the error code on a given graphql error extension
func Set(err *gqlerror.Error, value string) {
if err.Extensions == nil {
err.Extensions = map[string]interface{}{}
}
err.Extensions["code"] = value
}
// get the kind of the first non User error, defaults to User if no errors have a custom extension
func GetErrorKind(errs gqlerror.List) ErrorKind {
for _, err := range errs {
if code, ok := err.Extensions["code"].(string); ok {
if kind, ok := codeType[code]; ok && kind != KindUser {
return kind
}
}
}
return KindUser
}

View File

@@ -3,10 +3,10 @@ package graphql
import ( import (
"context" "context"
"github.com/vektah/gqlparser/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
) )
type ErrorPresenterFunc func(context.Context, error) *gqlerror.Error type ErrorPresenterFunc func(ctx context.Context, err error) *gqlerror.Error
type ExtendedError interface { type ExtendedError interface {
Extensions() map[string]interface{} Extensions() map[string]interface{}
@@ -15,7 +15,7 @@ type ExtendedError interface {
func DefaultErrorPresenter(ctx context.Context, err error) *gqlerror.Error { func DefaultErrorPresenter(ctx context.Context, err error) *gqlerror.Error {
if gqlerr, ok := err.(*gqlerror.Error); ok { if gqlerr, ok := err.(*gqlerror.Error); ok {
if gqlerr.Path == nil { if gqlerr.Path == nil {
gqlerr.Path = GetResolverContext(ctx).Path() gqlerr.Path = GetFieldContext(ctx).Path()
} }
return gqlerr return gqlerr
} }
@@ -27,7 +27,7 @@ func DefaultErrorPresenter(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{ return &gqlerror.Error{
Message: err.Error(), Message: err.Error(),
Path: GetResolverContext(ctx).Path(), Path: GetFieldContext(ctx).Path(),
Extensions: extensions, Extensions: extensions,
} }
} }

View File

@@ -1,29 +1,29 @@
//go:generate go run github.com/matryer/moq -out executable_schema_mock.go . ExecutableSchema
package graphql package graphql
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
) )
type ExecutableSchema interface { type ExecutableSchema interface {
Schema() *ast.Schema Schema() *ast.Schema
Complexity(typeName, fieldName string, childComplexity int, args map[string]interface{}) (int, bool) Complexity(typeName, fieldName string, childComplexity int, args map[string]interface{}) (int, bool)
Query(ctx context.Context, op *ast.OperationDefinition) *Response Exec(ctx context.Context) ResponseHandler
Mutation(ctx context.Context, op *ast.OperationDefinition) *Response
Subscription(ctx context.Context, op *ast.OperationDefinition) func() *Response
} }
// CollectFields returns the set of fields from an ast.SelectionSet where all collected fields satisfy at least one of the GraphQL types // CollectFields returns the set of fields from an ast.SelectionSet where all collected fields satisfy at least one of the GraphQL types
// passed through satisfies. Providing an empty or nil slice for satisfies will return collect all fields regardless of fragment // passed through satisfies. Providing an empty or nil slice for satisfies will return collect all fields regardless of fragment
// type conditions. // type conditions.
func CollectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []string) []CollectedField { func CollectFields(reqCtx *OperationContext, selSet ast.SelectionSet, satisfies []string) []CollectedField {
return collectFields(reqCtx, selSet, satisfies, map[string]bool{}) return collectFields(reqCtx, selSet, satisfies, map[string]bool{})
} }
func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []string, visited map[string]bool) []CollectedField { func collectFields(reqCtx *OperationContext, selSet ast.SelectionSet, satisfies []string, visited map[string]bool) []CollectedField {
groupedFields := make([]CollectedField, 0, len(selSet)) groupedFields := make([]CollectedField, 0, len(selSet))
for _, sel := range selSet { for _, sel := range selSet {
@@ -32,7 +32,7 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
if !shouldIncludeNode(sel.Directives, reqCtx.Variables) { if !shouldIncludeNode(sel.Directives, reqCtx.Variables) {
continue continue
} }
f := getOrCreateAndAppendField(&groupedFields, sel.Alias, func() CollectedField { f := getOrCreateAndAppendField(&groupedFields, sel.Alias, sel.ObjectDefinition, func() CollectedField {
return CollectedField{Field: sel} return CollectedField{Field: sel}
}) })
@@ -45,7 +45,7 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
continue continue
} }
for _, childField := range collectFields(reqCtx, sel.SelectionSet, satisfies, visited) { for _, childField := range collectFields(reqCtx, sel.SelectionSet, satisfies, visited) {
f := getOrCreateAndAppendField(&groupedFields, childField.Name, func() CollectedField { return childField }) f := getOrCreateAndAppendField(&groupedFields, childField.Name, childField.ObjectDefinition, func() CollectedField { return childField })
f.Selections = append(f.Selections, childField.Selections...) f.Selections = append(f.Selections, childField.Selections...)
} }
@@ -70,7 +70,7 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
} }
for _, childField := range collectFields(reqCtx, fragment.SelectionSet, satisfies, visited) { for _, childField := range collectFields(reqCtx, fragment.SelectionSet, satisfies, visited) {
f := getOrCreateAndAppendField(&groupedFields, childField.Name, func() CollectedField { return childField }) f := getOrCreateAndAppendField(&groupedFields, childField.Name, childField.ObjectDefinition, func() CollectedField { return childField })
f.Selections = append(f.Selections, childField.Selections...) f.Selections = append(f.Selections, childField.Selections...)
} }
default: default:
@@ -96,9 +96,9 @@ func instanceOf(val string, satisfies []string) bool {
return false return false
} }
func getOrCreateAndAppendField(c *[]CollectedField, name string, creator func() CollectedField) *CollectedField { func getOrCreateAndAppendField(c *[]CollectedField, name string, objectDefinition *ast.Definition, creator func() CollectedField) *CollectedField {
for i, cf := range *c { for i, cf := range *c {
if cf.Alias == name { if cf.Alias == name && (cf.ObjectDefinition == objectDefinition || (cf.ObjectDefinition != nil && objectDefinition != nil && cf.ObjectDefinition.Name == objectDefinition.Name)) {
return &(*c)[i] return &(*c)[i]
} }
} }

View File

@@ -0,0 +1,175 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package graphql
import (
"context"
"github.com/vektah/gqlparser/v2/ast"
"sync"
)
var (
lockExecutableSchemaMockComplexity sync.RWMutex
lockExecutableSchemaMockExec sync.RWMutex
lockExecutableSchemaMockSchema sync.RWMutex
)
// Ensure, that ExecutableSchemaMock does implement ExecutableSchema.
// If this is not the case, regenerate this file with moq.
var _ ExecutableSchema = &ExecutableSchemaMock{}
// ExecutableSchemaMock is a mock implementation of ExecutableSchema.
//
// func TestSomethingThatUsesExecutableSchema(t *testing.T) {
//
// // make and configure a mocked ExecutableSchema
// mockedExecutableSchema := &ExecutableSchemaMock{
// ComplexityFunc: func(typeName string, fieldName string, childComplexity int, args map[string]interface{}) (int, bool) {
// panic("mock out the Complexity method")
// },
// ExecFunc: func(ctx context.Context) ResponseHandler {
// panic("mock out the Exec method")
// },
// SchemaFunc: func() *ast.Schema {
// panic("mock out the Schema method")
// },
// }
//
// // use mockedExecutableSchema in code that requires ExecutableSchema
// // and then make assertions.
//
// }
type ExecutableSchemaMock struct {
// ComplexityFunc mocks the Complexity method.
ComplexityFunc func(typeName string, fieldName string, childComplexity int, args map[string]interface{}) (int, bool)
// ExecFunc mocks the Exec method.
ExecFunc func(ctx context.Context) ResponseHandler
// SchemaFunc mocks the Schema method.
SchemaFunc func() *ast.Schema
// calls tracks calls to the methods.
calls struct {
// Complexity holds details about calls to the Complexity method.
Complexity []struct {
// TypeName is the typeName argument value.
TypeName string
// FieldName is the fieldName argument value.
FieldName string
// ChildComplexity is the childComplexity argument value.
ChildComplexity int
// Args is the args argument value.
Args map[string]interface{}
}
// Exec holds details about calls to the Exec method.
Exec []struct {
// Ctx is the ctx argument value.
Ctx context.Context
}
// Schema holds details about calls to the Schema method.
Schema []struct {
}
}
}
// Complexity calls ComplexityFunc.
func (mock *ExecutableSchemaMock) Complexity(typeName string, fieldName string, childComplexity int, args map[string]interface{}) (int, bool) {
if mock.ComplexityFunc == nil {
panic("ExecutableSchemaMock.ComplexityFunc: method is nil but ExecutableSchema.Complexity was just called")
}
callInfo := struct {
TypeName string
FieldName string
ChildComplexity int
Args map[string]interface{}
}{
TypeName: typeName,
FieldName: fieldName,
ChildComplexity: childComplexity,
Args: args,
}
lockExecutableSchemaMockComplexity.Lock()
mock.calls.Complexity = append(mock.calls.Complexity, callInfo)
lockExecutableSchemaMockComplexity.Unlock()
return mock.ComplexityFunc(typeName, fieldName, childComplexity, args)
}
// ComplexityCalls gets all the calls that were made to Complexity.
// Check the length with:
// len(mockedExecutableSchema.ComplexityCalls())
func (mock *ExecutableSchemaMock) ComplexityCalls() []struct {
TypeName string
FieldName string
ChildComplexity int
Args map[string]interface{}
} {
var calls []struct {
TypeName string
FieldName string
ChildComplexity int
Args map[string]interface{}
}
lockExecutableSchemaMockComplexity.RLock()
calls = mock.calls.Complexity
lockExecutableSchemaMockComplexity.RUnlock()
return calls
}
// Exec calls ExecFunc.
func (mock *ExecutableSchemaMock) Exec(ctx context.Context) ResponseHandler {
if mock.ExecFunc == nil {
panic("ExecutableSchemaMock.ExecFunc: method is nil but ExecutableSchema.Exec was just called")
}
callInfo := struct {
Ctx context.Context
}{
Ctx: ctx,
}
lockExecutableSchemaMockExec.Lock()
mock.calls.Exec = append(mock.calls.Exec, callInfo)
lockExecutableSchemaMockExec.Unlock()
return mock.ExecFunc(ctx)
}
// ExecCalls gets all the calls that were made to Exec.
// Check the length with:
// len(mockedExecutableSchema.ExecCalls())
func (mock *ExecutableSchemaMock) ExecCalls() []struct {
Ctx context.Context
} {
var calls []struct {
Ctx context.Context
}
lockExecutableSchemaMockExec.RLock()
calls = mock.calls.Exec
lockExecutableSchemaMockExec.RUnlock()
return calls
}
// Schema calls SchemaFunc.
func (mock *ExecutableSchemaMock) Schema() *ast.Schema {
if mock.SchemaFunc == nil {
panic("ExecutableSchemaMock.SchemaFunc: method is nil but ExecutableSchema.Schema was just called")
}
callInfo := struct {
}{}
lockExecutableSchemaMockSchema.Lock()
mock.calls.Schema = append(mock.calls.Schema, callInfo)
lockExecutableSchemaMockSchema.Unlock()
return mock.SchemaFunc()
}
// SchemaCalls gets all the calls that were made to Schema.
// Check the length with:
// len(mockedExecutableSchema.SchemaCalls())
func (mock *ExecutableSchemaMock) SchemaCalls() []struct {
} {
var calls []struct {
}
lockExecutableSchemaMockSchema.RLock()
calls = mock.calls.Schema
lockExecutableSchemaMockSchema.RUnlock()
return calls
}

View File

@@ -0,0 +1,191 @@
package executor
import (
"context"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/vektah/gqlparser/v2/parser"
"github.com/vektah/gqlparser/v2/validator"
)
// Executor executes graphql queries against a schema.
type Executor struct {
es graphql.ExecutableSchema
extensions []graphql.HandlerExtension
ext extensions
errorPresenter graphql.ErrorPresenterFunc
recoverFunc graphql.RecoverFunc
queryCache graphql.Cache
}
var _ graphql.GraphExecutor = &Executor{}
// New creates a new Executor with the given schema, and a default error and
// recovery callbacks, and no query cache or extensions.
func New(es graphql.ExecutableSchema) *Executor {
e := &Executor{
es: es,
errorPresenter: graphql.DefaultErrorPresenter,
recoverFunc: graphql.DefaultRecover,
queryCache: graphql.NoCache{},
ext: processExtensions(nil),
}
return e
}
func (e *Executor) CreateOperationContext(ctx context.Context, params *graphql.RawParams) (*graphql.OperationContext, gqlerror.List) {
rc := &graphql.OperationContext{
DisableIntrospection: true,
Recover: e.recoverFunc,
ResolverMiddleware: e.ext.fieldMiddleware,
Stats: graphql.Stats{
Read: params.ReadTime,
OperationStart: graphql.GetStartTime(ctx),
},
}
ctx = graphql.WithOperationContext(ctx, rc)
for _, p := range e.ext.operationParameterMutators {
if err := p.MutateOperationParameters(ctx, params); err != nil {
return rc, gqlerror.List{err}
}
}
rc.RawQuery = params.Query
rc.OperationName = params.OperationName
var listErr gqlerror.List
rc.Doc, listErr = e.parseQuery(ctx, &rc.Stats, params.Query)
if len(listErr) != 0 {
return rc, listErr
}
rc.Operation = rc.Doc.Operations.ForName(params.OperationName)
if rc.Operation == nil {
return rc, gqlerror.List{gqlerror.Errorf("operation %s not found", params.OperationName)}
}
var err *gqlerror.Error
rc.Variables, err = validator.VariableValues(e.es.Schema(), rc.Operation, params.Variables)
if err != nil {
errcode.Set(err, errcode.ValidationFailed)
return rc, gqlerror.List{err}
}
rc.Stats.Validation.End = graphql.Now()
for _, p := range e.ext.operationContextMutators {
if err := p.MutateOperationContext(ctx, rc); err != nil {
return rc, gqlerror.List{err}
}
}
return rc, nil
}
func (e *Executor) DispatchOperation(ctx context.Context, rc *graphql.OperationContext) (graphql.ResponseHandler, context.Context) {
ctx = graphql.WithOperationContext(ctx, rc)
var innerCtx context.Context
res := e.ext.operationMiddleware(ctx, func(ctx context.Context) graphql.ResponseHandler {
innerCtx = ctx
tmpResponseContext := graphql.WithResponseContext(ctx, e.errorPresenter, e.recoverFunc)
responses := e.es.Exec(tmpResponseContext)
if errs := graphql.GetErrors(tmpResponseContext); errs != nil {
return graphql.OneShot(&graphql.Response{Errors: errs})
}
return func(ctx context.Context) *graphql.Response {
ctx = graphql.WithResponseContext(ctx, e.errorPresenter, e.recoverFunc)
resp := e.ext.responseMiddleware(ctx, func(ctx context.Context) *graphql.Response {
resp := responses(ctx)
if resp == nil {
return nil
}
resp.Errors = append(resp.Errors, graphql.GetErrors(ctx)...)
resp.Extensions = graphql.GetExtensions(ctx)
return resp
})
if resp == nil {
return nil
}
return resp
}
})
return res, innerCtx
}
func (e *Executor) DispatchError(ctx context.Context, list gqlerror.List) *graphql.Response {
ctx = graphql.WithResponseContext(ctx, e.errorPresenter, e.recoverFunc)
for _, gErr := range list {
graphql.AddError(ctx, gErr)
}
resp := e.ext.responseMiddleware(ctx, func(ctx context.Context) *graphql.Response {
resp := &graphql.Response{
Errors: list,
}
resp.Extensions = graphql.GetExtensions(ctx)
return resp
})
return resp
}
func (e *Executor) PresentRecoveredError(ctx context.Context, err interface{}) *gqlerror.Error {
return e.errorPresenter(ctx, e.recoverFunc(ctx, err))
}
func (e *Executor) SetQueryCache(cache graphql.Cache) {
e.queryCache = cache
}
func (e *Executor) SetErrorPresenter(f graphql.ErrorPresenterFunc) {
e.errorPresenter = f
}
func (e *Executor) SetRecoverFunc(f graphql.RecoverFunc) {
e.recoverFunc = f
}
// parseQuery decodes the incoming query and validates it, pulling from cache if present.
//
// NOTE: This should NOT look at variables, they will change per request. It should only parse and validate
// the raw query string.
func (e *Executor) parseQuery(ctx context.Context, stats *graphql.Stats, query string) (*ast.QueryDocument, gqlerror.List) {
stats.Parsing.Start = graphql.Now()
if doc, ok := e.queryCache.Get(ctx, query); ok {
now := graphql.Now()
stats.Parsing.End = now
stats.Validation.Start = now
return doc.(*ast.QueryDocument), nil
}
doc, err := parser.ParseQuery(&ast.Source{Input: query})
if err != nil {
errcode.Set(err, errcode.ParseFailed)
return nil, gqlerror.List{err}
}
stats.Parsing.End = graphql.Now()
stats.Validation.Start = graphql.Now()
listErr := validator.Validate(e.es.Schema(), doc)
if len(listErr) != 0 {
for _, e := range listErr {
errcode.Set(e, errcode.ValidationFailed)
}
return nil, listErr
}
e.queryCache.Add(ctx, query, doc)
return doc, nil
}

View File

@@ -0,0 +1,159 @@
package executor
import (
"context"
"fmt"
"github.com/99designs/gqlgen/graphql"
)
// Use adds the given extension to this Executor.
func (e *Executor) Use(extension graphql.HandlerExtension) {
if err := extension.Validate(e.es); err != nil {
panic(err)
}
switch extension.(type) {
case graphql.OperationParameterMutator,
graphql.OperationContextMutator,
graphql.OperationInterceptor,
graphql.FieldInterceptor,
graphql.ResponseInterceptor:
e.extensions = append(e.extensions, extension)
e.ext = processExtensions(e.extensions)
default:
panic(fmt.Errorf("cannot Use %T as a gqlgen handler extension because it does not implement any extension hooks", extension))
}
}
// AroundFields is a convenience method for creating an extension that only implements field middleware
func (e *Executor) AroundFields(f graphql.FieldMiddleware) {
e.Use(aroundFieldFunc(f))
}
// AroundOperations is a convenience method for creating an extension that only implements operation middleware
func (e *Executor) AroundOperations(f graphql.OperationMiddleware) {
e.Use(aroundOpFunc(f))
}
// AroundResponses is a convenience method for creating an extension that only implements response middleware
func (e *Executor) AroundResponses(f graphql.ResponseMiddleware) {
e.Use(aroundRespFunc(f))
}
type extensions struct {
operationMiddleware graphql.OperationMiddleware
responseMiddleware graphql.ResponseMiddleware
fieldMiddleware graphql.FieldMiddleware
operationParameterMutators []graphql.OperationParameterMutator
operationContextMutators []graphql.OperationContextMutator
}
func processExtensions(exts []graphql.HandlerExtension) extensions {
e := extensions{
operationMiddleware: func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
return next(ctx)
},
responseMiddleware: func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
return next(ctx)
},
fieldMiddleware: func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
return next(ctx)
},
}
// this loop goes backwards so the first extension is the outer most middleware and runs first.
for i := len(exts) - 1; i >= 0; i-- {
p := exts[i]
if p, ok := p.(graphql.OperationInterceptor); ok {
previous := e.operationMiddleware
e.operationMiddleware = func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
return p.InterceptOperation(ctx, func(ctx context.Context) graphql.ResponseHandler {
return previous(ctx, next)
})
}
}
if p, ok := p.(graphql.ResponseInterceptor); ok {
previous := e.responseMiddleware
e.responseMiddleware = func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
return p.InterceptResponse(ctx, func(ctx context.Context) *graphql.Response {
return previous(ctx, next)
})
}
}
if p, ok := p.(graphql.FieldInterceptor); ok {
previous := e.fieldMiddleware
e.fieldMiddleware = func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
return p.InterceptField(ctx, func(ctx context.Context) (res interface{}, err error) {
return previous(ctx, next)
})
}
}
}
for _, p := range exts {
if p, ok := p.(graphql.OperationParameterMutator); ok {
e.operationParameterMutators = append(e.operationParameterMutators, p)
}
if p, ok := p.(graphql.OperationContextMutator); ok {
e.operationContextMutators = append(e.operationContextMutators, p)
}
}
return e
}
type aroundOpFunc func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler
func (r aroundOpFunc) ExtensionName() string {
return "InlineOperationFunc"
}
func (r aroundOpFunc) Validate(schema graphql.ExecutableSchema) error {
if r == nil {
return fmt.Errorf("OperationFunc can not be nil")
}
return nil
}
func (r aroundOpFunc) InterceptOperation(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
return r(ctx, next)
}
type aroundRespFunc func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response
func (r aroundRespFunc) ExtensionName() string {
return "InlineResponseFunc"
}
func (r aroundRespFunc) Validate(schema graphql.ExecutableSchema) error {
if r == nil {
return fmt.Errorf("ResponseFunc can not be nil")
}
return nil
}
func (r aroundRespFunc) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
return r(ctx, next)
}
type aroundFieldFunc func(ctx context.Context, next graphql.Resolver) (res interface{}, err error)
func (f aroundFieldFunc) ExtensionName() string {
return "InlineFieldFunc"
}
func (f aroundFieldFunc) Validate(schema graphql.ExecutableSchema) error {
if f == nil {
return fmt.Errorf("FieldFunc can not be nil")
}
return nil
}
func (f aroundFieldFunc) InterceptField(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
return f(ctx, next)
}

123
vendor/github.com/99designs/gqlgen/graphql/handler.go generated vendored Normal file
View File

@@ -0,0 +1,123 @@
package graphql
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type (
OperationMiddleware func(ctx context.Context, next OperationHandler) ResponseHandler
OperationHandler func(ctx context.Context) ResponseHandler
ResponseHandler func(ctx context.Context) *Response
ResponseMiddleware func(ctx context.Context, next ResponseHandler) *Response
Resolver func(ctx context.Context) (res interface{}, err error)
FieldMiddleware func(ctx context.Context, next Resolver) (res interface{}, err error)
RawParams struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
Extensions map[string]interface{} `json:"extensions"`
ReadTime TraceTiming `json:"-"`
}
GraphExecutor interface {
CreateOperationContext(ctx context.Context, params *RawParams) (*OperationContext, gqlerror.List)
DispatchOperation(ctx context.Context, rc *OperationContext) (ResponseHandler, context.Context)
DispatchError(ctx context.Context, list gqlerror.List) *Response
}
// HandlerExtension adds functionality to the http handler. See the list of possible hook points below
// Its important to understand the lifecycle of a graphql request and the terminology we use in gqlgen
// before working with these
//
// +--- REQUEST POST /graphql --------------------------------------------+
// | +- OPERATION query OpName { viewer { name } } -----------------------+ |
// | | RESPONSE { "data": { "viewer": { "name": "bob" } } } | |
// | +- OPERATION subscription OpName2 { chat { message } } --------------+ |
// | | RESPONSE { "data": { "chat": { "message": "hello" } } } | |
// | | RESPONSE { "data": { "chat": { "message": "byee" } } } | |
// | +--------------------------------------------------------------------+ |
// +------------------------------------------------------------------------+
HandlerExtension interface {
// ExtensionName should be a CamelCase string version of the extension which may be shown in stats and logging.
ExtensionName() string
// Validate is called when adding an extension to the server, it allows validation against the servers schema.
Validate(schema ExecutableSchema) error
}
// OperationParameterMutator is called before creating a request context. allows manipulating the raw query
// on the way in.
OperationParameterMutator interface {
MutateOperationParameters(ctx context.Context, request *RawParams) *gqlerror.Error
}
// OperationContextMutator is called after creating the request context, but before executing the root resolver.
OperationContextMutator interface {
MutateOperationContext(ctx context.Context, rc *OperationContext) *gqlerror.Error
}
// OperationInterceptor is called for each incoming query, for basic requests the writer will be invoked once,
// for subscriptions it will be invoked multiple times.
OperationInterceptor interface {
InterceptOperation(ctx context.Context, next OperationHandler) ResponseHandler
}
// ResponseInterceptor is called around each graphql operation response. This can be called many times for a single
// operation the case of subscriptions.
ResponseInterceptor interface {
InterceptResponse(ctx context.Context, next ResponseHandler) *Response
}
// FieldInterceptor called around each field
FieldInterceptor interface {
InterceptField(ctx context.Context, next Resolver) (res interface{}, err error)
}
// Transport provides support for different wire level encodings of graphql requests, eg Form, Get, Post, Websocket
Transport interface {
Supports(r *http.Request) bool
Do(w http.ResponseWriter, r *http.Request, exec GraphExecutor)
}
)
type Status int
func (p *RawParams) AddUpload(upload Upload, key, path string) *gqlerror.Error {
if !strings.HasPrefix(path, "variables.") {
return gqlerror.Errorf("invalid operations paths for key %s", key)
}
var ptr interface{} = p.Variables
parts := strings.Split(path, ".")
// skip the first part (variables) because we started there
for i, p := range parts[1:] {
last := i == len(parts)-2
if ptr == nil {
return gqlerror.Errorf("path is missing \"variables.\" prefix, key: %s, path: %s", key, path)
}
if index, parseNbrErr := strconv.Atoi(p); parseNbrErr == nil {
if last {
ptr.([]interface{})[index] = upload
} else {
ptr = ptr.([]interface{})[index]
}
} else {
if last {
ptr.(map[string]interface{})[p] = upload
} else {
ptr = ptr.(map[string]interface{})[p]
}
}
}
return nil
}

View File

@@ -0,0 +1,112 @@
package extension
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/99designs/gqlgen/graphql"
"github.com/mitchellh/mapstructure"
)
const errPersistedQueryNotFound = "PersistedQueryNotFound"
const errPersistedQueryNotFoundCode = "PERSISTED_QUERY_NOT_FOUND"
// AutomaticPersistedQuery saves client upload by optimistically sending only the hashes of queries, if the server
// does not yet know what the query is for the hash it will respond telling the client to send the query along with the
// hash in the next request.
// see https://github.com/apollographql/apollo-link-persisted-queries
type AutomaticPersistedQuery struct {
Cache graphql.Cache
}
type ApqStats struct {
// The hash of the incoming query
Hash string
// SentQuery is true if the incoming request sent the full query
SentQuery bool
}
const apqExtension = "APQ"
var _ interface {
graphql.OperationParameterMutator
graphql.HandlerExtension
} = AutomaticPersistedQuery{}
func (a AutomaticPersistedQuery) ExtensionName() string {
return "AutomaticPersistedQuery"
}
func (a AutomaticPersistedQuery) Validate(schema graphql.ExecutableSchema) error {
if a.Cache == nil {
return fmt.Errorf("AutomaticPersistedQuery.Cache can not be nil")
}
return nil
}
func (a AutomaticPersistedQuery) MutateOperationParameters(ctx context.Context, rawParams *graphql.RawParams) *gqlerror.Error {
if rawParams.Extensions["persistedQuery"] == nil {
return nil
}
var extension struct {
Sha256 string `mapstructure:"sha256Hash"`
Version int64 `mapstructure:"version"`
}
if err := mapstructure.Decode(rawParams.Extensions["persistedQuery"], &extension); err != nil {
return gqlerror.Errorf("invalid APQ extension data")
}
if extension.Version != 1 {
return gqlerror.Errorf("unsupported APQ version")
}
fullQuery := false
if rawParams.Query == "" {
// client sent optimistic query hash without query string, get it from the cache
query, ok := a.Cache.Get(ctx, extension.Sha256)
if !ok {
err := gqlerror.Errorf(errPersistedQueryNotFound)
errcode.Set(err, errPersistedQueryNotFoundCode)
return err
}
rawParams.Query = query.(string)
} else {
// client sent optimistic query hash with query string, verify and store it
if computeQueryHash(rawParams.Query) != extension.Sha256 {
return gqlerror.Errorf("provided APQ hash does not match query")
}
a.Cache.Add(ctx, extension.Sha256, rawParams.Query)
fullQuery = true
}
graphql.GetOperationContext(ctx).Stats.SetExtension(apqExtension, &ApqStats{
Hash: extension.Sha256,
SentQuery: fullQuery,
})
return nil
}
func GetApqStats(ctx context.Context) *ApqStats {
rc := graphql.GetOperationContext(ctx)
if rc == nil {
return nil
}
s, _ := rc.Stats.GetExtension(apqExtension).(*ApqStats)
return s
}
func computeQueryHash(query string) string {
b := sha256.Sum256([]byte(query))
return hex.EncodeToString(b[:])
}

View File

@@ -0,0 +1,88 @@
package extension
import (
"context"
"fmt"
"github.com/99designs/gqlgen/complexity"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/vektah/gqlparser/v2/gqlerror"
)
const errComplexityLimit = "COMPLEXITY_LIMIT_EXCEEDED"
// ComplexityLimit allows you to define a limit on query complexity
//
// If a query is submitted that exceeds the limit, a 422 status code will be returned.
type ComplexityLimit struct {
Func func(ctx context.Context, rc *graphql.OperationContext) int
es graphql.ExecutableSchema
}
var _ interface {
graphql.OperationContextMutator
graphql.HandlerExtension
} = &ComplexityLimit{}
const complexityExtension = "ComplexityLimit"
type ComplexityStats struct {
// The calculated complexity for this request
Complexity int
// The complexity limit for this request returned by the extension func
ComplexityLimit int
}
// FixedComplexityLimit sets a complexity limit that does not change
func FixedComplexityLimit(limit int) *ComplexityLimit {
return &ComplexityLimit{
Func: func(ctx context.Context, rc *graphql.OperationContext) int {
return limit
},
}
}
func (c ComplexityLimit) ExtensionName() string {
return complexityExtension
}
func (c *ComplexityLimit) Validate(schema graphql.ExecutableSchema) error {
if c.Func == nil {
return fmt.Errorf("ComplexityLimit func can not be nil")
}
c.es = schema
return nil
}
func (c ComplexityLimit) MutateOperationContext(ctx context.Context, rc *graphql.OperationContext) *gqlerror.Error {
op := rc.Doc.Operations.ForName(rc.OperationName)
complexity := complexity.Calculate(c.es, op, rc.Variables)
limit := c.Func(ctx, rc)
rc.Stats.SetExtension(complexityExtension, &ComplexityStats{
Complexity: complexity,
ComplexityLimit: limit,
})
if complexity > limit {
err := gqlerror.Errorf("operation has complexity %d, which exceeds the limit of %d", complexity, limit)
errcode.Set(err, errComplexityLimit)
return err
}
return nil
}
func GetComplexityStats(ctx context.Context) *ComplexityStats {
rc := graphql.GetOperationContext(ctx)
if rc == nil {
return nil
}
s, _ := rc.Stats.GetExtension(complexityExtension).(*ComplexityStats)
return s
}

View File

@@ -0,0 +1,29 @@
package extension
import (
"context"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// EnableIntrospection enables clients to reflect all of the types available on the graph.
type Introspection struct{}
var _ interface {
graphql.OperationContextMutator
graphql.HandlerExtension
} = Introspection{}
func (c Introspection) ExtensionName() string {
return "Introspection"
}
func (c Introspection) Validate(schema graphql.ExecutableSchema) error {
return nil
}
func (c Introspection) MutateOperationContext(ctx context.Context, rc *graphql.OperationContext) *gqlerror.Error {
rc.DisableIntrospection = false
return nil
}

View File

@@ -0,0 +1,32 @@
package lru
import (
"context"
"github.com/99designs/gqlgen/graphql"
lru "github.com/hashicorp/golang-lru"
)
type LRU struct {
lru *lru.Cache
}
var _ graphql.Cache = &LRU{}
func New(size int) *LRU {
cache, err := lru.New(size)
if err != nil {
// An error is only returned for non-positive cache size
// and we already checked for that.
panic("unexpected error creating cache: " + err.Error())
}
return &LRU{cache}
}
func (l LRU) Get(ctx context.Context, key string) (value interface{}, ok bool) {
return l.lru.Get(key)
}
func (l LRU) Add(ctx context.Context, key string, value interface{}) {
l.lru.Add(key, value)
}

View File

@@ -0,0 +1,180 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/executor"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type (
Server struct {
transports []graphql.Transport
exec *executor.Executor
}
)
func New(es graphql.ExecutableSchema) *Server {
return &Server{
exec: executor.New(es),
}
}
func NewDefaultServer(es graphql.ExecutableSchema) *Server {
srv := New(es)
srv.AddTransport(transport.Websocket{
KeepAlivePingInterval: 10 * time.Second,
})
srv.AddTransport(transport.Options{})
srv.AddTransport(transport.GET{})
srv.AddTransport(transport.POST{})
srv.AddTransport(transport.MultipartForm{})
srv.SetQueryCache(lru.New(1000))
srv.Use(extension.Introspection{})
srv.Use(extension.AutomaticPersistedQuery{
Cache: lru.New(100),
})
return srv
}
func (s *Server) AddTransport(transport graphql.Transport) {
s.transports = append(s.transports, transport)
}
func (s *Server) SetErrorPresenter(f graphql.ErrorPresenterFunc) {
s.exec.SetErrorPresenter(f)
}
func (s *Server) SetRecoverFunc(f graphql.RecoverFunc) {
s.exec.SetRecoverFunc(f)
}
func (s *Server) SetQueryCache(cache graphql.Cache) {
s.exec.SetQueryCache(cache)
}
func (s *Server) Use(extension graphql.HandlerExtension) {
s.exec.Use(extension)
}
// AroundFields is a convenience method for creating an extension that only implements field middleware
func (s *Server) AroundFields(f graphql.FieldMiddleware) {
s.exec.AroundFields(f)
}
// AroundOperations is a convenience method for creating an extension that only implements operation middleware
func (s *Server) AroundOperations(f graphql.OperationMiddleware) {
s.exec.AroundOperations(f)
}
// AroundResponses is a convenience method for creating an extension that only implements response middleware
func (s *Server) AroundResponses(f graphql.ResponseMiddleware) {
s.exec.AroundResponses(f)
}
func (s *Server) getTransport(r *http.Request) graphql.Transport {
for _, t := range s.transports {
if t.Supports(r) {
return t
}
}
return nil
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
err := s.exec.PresentRecoveredError(r.Context(), err)
resp := &graphql.Response{Errors: []*gqlerror.Error{err}}
b, _ := json.Marshal(resp)
w.WriteHeader(http.StatusUnprocessableEntity)
w.Write(b)
}
}()
r = r.WithContext(graphql.StartOperationTrace(r.Context()))
transport := s.getTransport(r)
if transport == nil {
sendErrorf(w, http.StatusBadRequest, "transport not supported")
return
}
transport.Do(w, r, s.exec)
}
func sendError(w http.ResponseWriter, code int, errors ...*gqlerror.Error) {
w.WriteHeader(code)
b, err := json.Marshal(&graphql.Response{Errors: errors})
if err != nil {
panic(err)
}
w.Write(b)
}
func sendErrorf(w http.ResponseWriter, code int, format string, args ...interface{}) {
sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)})
}
type OperationFunc func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler
func (r OperationFunc) ExtensionName() string {
return "InlineOperationFunc"
}
func (r OperationFunc) Validate(schema graphql.ExecutableSchema) error {
if r == nil {
return fmt.Errorf("OperationFunc can not be nil")
}
return nil
}
func (r OperationFunc) InterceptOperation(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
return r(ctx, next)
}
type ResponseFunc func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response
func (r ResponseFunc) ExtensionName() string {
return "InlineResponseFunc"
}
func (r ResponseFunc) Validate(schema graphql.ExecutableSchema) error {
if r == nil {
return fmt.Errorf("ResponseFunc can not be nil")
}
return nil
}
func (r ResponseFunc) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
return r(ctx, next)
}
type FieldFunc func(ctx context.Context, next graphql.Resolver) (res interface{}, err error)
func (f FieldFunc) ExtensionName() string {
return "InlineFieldFunc"
}
func (f FieldFunc) Validate(schema graphql.ExecutableSchema) error {
if f == nil {
return fmt.Errorf("FieldFunc can not be nil")
}
return nil
}
func (f FieldFunc) InterceptField(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
return f(ctx, next)
}

View File

@@ -0,0 +1,26 @@
package transport
import (
"encoding/json"
"fmt"
"net/http"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// SendError sends a best effort error to a raw response writer. It assumes the client can understand the standard
// json error response
func SendError(w http.ResponseWriter, code int, errors ...*gqlerror.Error) {
w.WriteHeader(code)
b, err := json.Marshal(&graphql.Response{Errors: errors})
if err != nil {
panic(err)
}
w.Write(b)
}
// SendErrorf wraps SendError to add formatted messages
func SendErrorf(w http.ResponseWriter, code int, format string, args ...interface{}) {
SendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)})
}

View File

@@ -0,0 +1,208 @@
package transport
import (
"encoding/json"
"io"
"io/ioutil"
"mime"
"net/http"
"os"
"strings"
"github.com/99designs/gqlgen/graphql"
)
// MultipartForm the Multipart request spec https://github.com/jaydenseric/graphql-multipart-request-spec
type MultipartForm struct {
// MaxUploadSize sets the maximum number of bytes used to parse a request body
// as multipart/form-data.
MaxUploadSize int64
// MaxMemory defines the maximum number of bytes used to parse a request body
// as multipart/form-data in memory, with the remainder stored on disk in
// temporary files.
MaxMemory int64
}
var _ graphql.Transport = MultipartForm{}
func (f MultipartForm) Supports(r *http.Request) bool {
if r.Header.Get("Upgrade") != "" {
return false
}
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return false
}
return r.Method == "POST" && mediaType == "multipart/form-data"
}
func (f MultipartForm) maxUploadSize() int64 {
if f.MaxUploadSize == 0 {
return 32 << 20
}
return f.MaxUploadSize
}
func (f MultipartForm) maxMemory() int64 {
if f.MaxMemory == 0 {
return 32 << 20
}
return f.MaxMemory
}
func (f MultipartForm) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
w.Header().Set("Content-Type", "application/json")
start := graphql.Now()
var err error
if r.ContentLength > f.maxUploadSize() {
writeJsonError(w, "failed to parse multipart form, request body too large")
return
}
r.Body = http.MaxBytesReader(w, r.Body, f.maxUploadSize())
if err = r.ParseMultipartForm(f.maxMemory()); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
if strings.Contains(err.Error(), "request body too large") {
writeJsonError(w, "failed to parse multipart form, request body too large")
return
}
writeJsonError(w, "failed to parse multipart form")
return
}
defer r.Body.Close()
var params graphql.RawParams
if err = jsonDecode(strings.NewReader(r.Form.Get("operations")), &params); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonError(w, "operations form field could not be decoded")
return
}
var uploadsMap = map[string][]string{}
if err = json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonError(w, "map form field could not be decoded")
return
}
var upload graphql.Upload
for key, paths := range uploadsMap {
if len(paths) == 0 {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "invalid empty operations paths list for key %s", key)
return
}
file, header, err := r.FormFile(key)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to get key %s from form", key)
return
}
defer file.Close()
if len(paths) == 1 {
upload = graphql.Upload{
File: file,
Size: header.Size,
Filename: header.Filename,
ContentType: header.Header.Get("Content-Type"),
}
if err := params.AddUpload(upload, key, paths[0]); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
} else {
if r.ContentLength < f.maxMemory() {
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to read file for key %s", key)
return
}
for _, path := range paths {
upload = graphql.Upload{
File: &bytesReader{s: &fileBytes, i: 0, prevRune: -1},
Size: header.Size,
Filename: header.Filename,
ContentType: header.Header.Get("Content-Type"),
}
if err := params.AddUpload(upload, key, path); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
}
} else {
tmpFile, err := ioutil.TempFile(os.TempDir(), "gqlgen-")
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to create temp file for key %s", key)
return
}
tmpName := tmpFile.Name()
defer func() {
_ = os.Remove(tmpName)
}()
_, err = io.Copy(tmpFile, file)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
if err := tmpFile.Close(); err != nil {
writeJsonErrorf(w, "failed to copy to temp file and close temp file for key %s", key)
return
}
writeJsonErrorf(w, "failed to copy to temp file for key %s", key)
return
}
if err := tmpFile.Close(); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to close temp file for key %s", key)
return
}
for _, path := range paths {
pathTmpFile, err := os.Open(tmpName)
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonErrorf(w, "failed to open temp file for key %s", key)
return
}
defer pathTmpFile.Close()
upload = graphql.Upload{
File: pathTmpFile,
Size: header.Size,
Filename: header.Filename,
ContentType: header.Header.Get("Content-Type"),
}
if err := params.AddUpload(upload, key, path); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
writeJsonGraphqlError(w, err)
return
}
}
}
}
}
params.ReadTime = graphql.TraceTiming{
Start: start,
End: graphql.Now(),
}
rc, gerr := exec.CreateOperationContext(r.Context(), &params)
if gerr != nil {
resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), gerr)
w.WriteHeader(statusFor(gerr))
writeJson(w, resp)
return
}
responses, ctx := exec.DispatchOperation(r.Context(), rc)
writeJson(w, responses(ctx))
}

View File

@@ -0,0 +1,87 @@
package transport
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
// GET implements the GET side of the default HTTP transport
// defined in https://github.com/APIs-guru/graphql-over-http#get
type GET struct{}
var _ graphql.Transport = GET{}
func (h GET) Supports(r *http.Request) bool {
if r.Header.Get("Upgrade") != "" {
return false
}
return r.Method == "GET"
}
func (h GET) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
w.Header().Set("Content-Type", "application/json")
raw := &graphql.RawParams{
Query: r.URL.Query().Get("query"),
OperationName: r.URL.Query().Get("operationName"),
}
raw.ReadTime.Start = graphql.Now()
if variables := r.URL.Query().Get("variables"); variables != "" {
if err := jsonDecode(strings.NewReader(variables), &raw.Variables); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonError(w, "variables could not be decoded")
return
}
}
if extensions := r.URL.Query().Get("extensions"); extensions != "" {
if err := jsonDecode(strings.NewReader(extensions), &raw.Extensions); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonError(w, "extensions could not be decoded")
return
}
}
raw.ReadTime.End = graphql.Now()
rc, err := exec.CreateOperationContext(r.Context(), raw)
if err != nil {
w.WriteHeader(statusFor(err))
resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), err)
writeJson(w, resp)
return
}
op := rc.Doc.Operations.ForName(rc.OperationName)
if op.Operation != ast.Query {
w.WriteHeader(http.StatusNotAcceptable)
writeJsonError(w, "GET requests only allow query operations")
return
}
responses, ctx := exec.DispatchOperation(r.Context(), rc)
writeJson(w, responses(ctx))
}
func jsonDecode(r io.Reader, val interface{}) error {
dec := json.NewDecoder(r)
dec.UseNumber()
return dec.Decode(val)
}
func statusFor(errs gqlerror.List) int {
switch errcode.GetErrorKind(errs) {
case errcode.KindProtocol:
return http.StatusUnprocessableEntity
default:
return http.StatusOK
}
}

View File

@@ -0,0 +1,54 @@
package transport
import (
"mime"
"net/http"
"github.com/99designs/gqlgen/graphql"
)
// POST implements the POST side of the default HTTP transport
// defined in https://github.com/APIs-guru/graphql-over-http#post
type POST struct{}
var _ graphql.Transport = POST{}
func (h POST) Supports(r *http.Request) bool {
if r.Header.Get("Upgrade") != "" {
return false
}
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
return false
}
return r.Method == "POST" && mediaType == "application/json"
}
func (h POST) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
w.Header().Set("Content-Type", "application/json")
var params *graphql.RawParams
start := graphql.Now()
if err := jsonDecode(r.Body, &params); err != nil {
w.WriteHeader(http.StatusBadRequest)
writeJsonErrorf(w, "json body could not be decoded: "+err.Error())
return
}
params.ReadTime = graphql.TraceTiming{
Start: start,
End: graphql.Now(),
}
rc, err := exec.CreateOperationContext(r.Context(), params)
if err != nil {
w.WriteHeader(statusFor(err))
resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), err)
writeJson(w, resp)
return
}
ctx := graphql.WithOperationContext(r.Context(), rc)
responses, ctx := exec.DispatchOperation(ctx, rc)
writeJson(w, responses(ctx))
}

View File

@@ -0,0 +1,26 @@
package transport
import (
"net/http"
"github.com/99designs/gqlgen/graphql"
)
// Options responds to http OPTIONS and HEAD requests
type Options struct{}
var _ graphql.Transport = Options{}
func (o Options) Supports(r *http.Request) bool {
return r.Method == "HEAD" || r.Method == "OPTIONS"
}
func (o Options) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
switch r.Method {
case http.MethodOptions:
w.WriteHeader(http.StatusOK)
w.Header().Set("Allow", "OPTIONS, GET, POST")
case http.MethodHead:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}

View File

@@ -0,0 +1,25 @@
package transport
import (
"errors"
"io"
)
type bytesReader struct {
s *[]byte
i int64 // current reading index
prevRune int // index of previous rune; or < 0
}
func (r *bytesReader) Read(b []byte) (n int, err error) {
if r.s == nil {
return 0, errors.New("byte slice pointer is nil")
}
if r.i >= int64(len(*r.s)) {
return 0, io.EOF
}
r.prevRune = -1
n = copy(b, (*r.s)[r.i:])
r.i += int64(n)
return
}

View File

@@ -0,0 +1,30 @@
package transport
import (
"encoding/json"
"fmt"
"io"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
)
func writeJson(w io.Writer, response *graphql.Response) {
b, err := json.Marshal(response)
if err != nil {
panic(err)
}
w.Write(b)
}
func writeJsonError(w io.Writer, msg string) {
writeJson(w, &graphql.Response{Errors: gqlerror.List{{Message: msg}}})
}
func writeJsonErrorf(w io.Writer, format string, args ...interface{}) {
writeJson(w, &graphql.Response{Errors: gqlerror.List{{Message: fmt.Sprintf(format, args...)}}})
}
func writeJsonGraphqlError(w io.Writer, err ...*gqlerror.Error) {
writeJson(w, &graphql.Response{Errors: err})
}

View File

@@ -1,4 +1,4 @@
package handler package transport
import ( import (
"bytes" "bytes"
@@ -11,12 +11,9 @@ import (
"time" "time"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/hashicorp/golang-lru" "github.com/vektah/gqlparser/v2/gqlerror"
"github.com/vektah/gqlparser"
"github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/gqlerror"
"github.com/vektah/gqlparser/validator"
) )
const ( const (
@@ -32,42 +29,53 @@ const (
connectionKeepAliveMsg = "ka" // Server -> Client connectionKeepAliveMsg = "ka" // Server -> Client
) )
type operationMessage struct { type (
Payload json.RawMessage `json:"payload,omitempty"` Websocket struct {
ID string `json:"id,omitempty"` Upgrader websocket.Upgrader
Type string `json:"type"` InitFunc WebsocketInitFunc
KeepAlivePingInterval time.Duration
}
wsConnection struct {
Websocket
ctx context.Context
conn *websocket.Conn
active map[string]context.CancelFunc
mu sync.Mutex
keepAliveTicker *time.Ticker
exec graphql.GraphExecutor
initPayload InitPayload
}
operationMessage struct {
Payload json.RawMessage `json:"payload,omitempty"`
ID string `json:"id,omitempty"`
Type string `json:"type"`
}
WebsocketInitFunc func(ctx context.Context, initPayload InitPayload) (context.Context, error)
)
var _ graphql.Transport = Websocket{}
func (t Websocket) Supports(r *http.Request) bool {
return r.Header.Get("Upgrade") != ""
} }
type wsConnection struct { func (t Websocket) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
ctx context.Context ws, err := t.Upgrader.Upgrade(w, r, http.Header{
conn *websocket.Conn
exec graphql.ExecutableSchema
active map[string]context.CancelFunc
mu sync.Mutex
cfg *Config
cache *lru.Cache
keepAliveTicker *time.Ticker
initPayload InitPayload
}
func connectWs(exec graphql.ExecutableSchema, w http.ResponseWriter, r *http.Request, cfg *Config, cache *lru.Cache) {
ws, err := cfg.upgrader.Upgrade(w, r, http.Header{
"Sec-Websocket-Protocol": []string{"graphql-ws"}, "Sec-Websocket-Protocol": []string{"graphql-ws"},
}) })
if err != nil { if err != nil {
log.Printf("unable to upgrade %T to websocket %s: ", w, err.Error()) log.Printf("unable to upgrade %T to websocket %s: ", w, err.Error())
sendErrorf(w, http.StatusBadRequest, "unable to upgrade") SendErrorf(w, http.StatusBadRequest, "unable to upgrade")
return return
} }
conn := wsConnection{ conn := wsConnection{
active: map[string]context.CancelFunc{}, active: map[string]context.CancelFunc{},
exec: exec, conn: ws,
conn: ws, ctx: r.Context(),
ctx: r.Context(), exec: exec,
cfg: cfg, Websocket: t,
cache: cache,
} }
if !conn.init() { if !conn.init() {
@@ -94,7 +102,18 @@ func (c *wsConnection) init() bool {
} }
} }
if c.InitFunc != nil {
ctx, err := c.InitFunc(c.ctx, c.initPayload)
if err != nil {
c.sendConnectionError(err.Error())
c.close(websocket.CloseNormalClosure, "terminated")
return false
}
c.ctx = ctx
}
c.write(&operationMessage{Type: connectionAckMsg}) c.write(&operationMessage{Type: connectionAckMsg})
c.write(&operationMessage{Type: connectionKeepAliveMsg})
case connectionTerminateMsg: case connectionTerminateMsg:
c.close(websocket.CloseNormalClosure, "terminated") c.close(websocket.CloseNormalClosure, "terminated")
return false return false
@@ -117,18 +136,22 @@ func (c *wsConnection) run() {
// We create a cancellation that will shutdown the keep-alive when we leave // We create a cancellation that will shutdown the keep-alive when we leave
// this function. // this function.
ctx, cancel := context.WithCancel(c.ctx) ctx, cancel := context.WithCancel(c.ctx)
defer cancel() defer func() {
cancel()
c.close(websocket.CloseAbnormalClosure, "unexpected closure")
}()
// Create a timer that will fire every interval to keep the connection alive. // Create a timer that will fire every interval to keep the connection alive.
if c.cfg.connectionKeepAlivePingInterval != 0 { if c.KeepAlivePingInterval != 0 {
c.mu.Lock() c.mu.Lock()
c.keepAliveTicker = time.NewTicker(c.cfg.connectionKeepAlivePingInterval) c.keepAliveTicker = time.NewTicker(c.KeepAlivePingInterval)
c.mu.Unlock() c.mu.Unlock()
go c.keepAlive(ctx) go c.keepAlive(ctx)
} }
for { for {
start := graphql.Now()
message := c.readOp() message := c.readOp()
if message == nil { if message == nil {
return return
@@ -136,19 +159,14 @@ func (c *wsConnection) run() {
switch message.Type { switch message.Type {
case startMsg: case startMsg:
if !c.subscribe(message) { c.subscribe(start, message)
return
}
case stopMsg: case stopMsg:
c.mu.Lock() c.mu.Lock()
closer := c.active[message.ID] closer := c.active[message.ID]
c.mu.Unlock() c.mu.Unlock()
if closer == nil { if closer != nil {
c.sendError(message.ID, gqlerror.Errorf("%s is not running, cannot stop", message.ID)) closer()
continue
} }
closer()
case connectionTerminateMsg: case connectionTerminateMsg:
c.close(websocket.CloseNormalClosure, "terminated") c.close(websocket.CloseNormalClosure, "terminated")
return return
@@ -172,108 +190,90 @@ func (c *wsConnection) keepAlive(ctx context.Context) {
} }
} }
func (c *wsConnection) subscribe(message *operationMessage) bool { func (c *wsConnection) subscribe(start time.Time, message *operationMessage) {
var reqParams params ctx := graphql.StartOperationTrace(c.ctx)
if err := jsonDecode(bytes.NewReader(message.Payload), &reqParams); err != nil { var params *graphql.RawParams
c.sendConnectionError("invalid json") if err := jsonDecode(bytes.NewReader(message.Payload), &params); err != nil {
return false c.sendError(message.ID, &gqlerror.Error{Message: "invalid json"})
c.complete(message.ID)
return
} }
var ( params.ReadTime = graphql.TraceTiming{
doc *ast.QueryDocument Start: start,
cacheHit bool End: graphql.Now(),
)
if c.cache != nil {
val, ok := c.cache.Get(reqParams.Query)
if ok {
doc = val.(*ast.QueryDocument)
cacheHit = true
}
}
if !cacheHit {
var qErr gqlerror.List
doc, qErr = gqlparser.LoadQuery(c.exec.Schema(), reqParams.Query)
if qErr != nil {
c.sendError(message.ID, qErr...)
return true
}
if c.cache != nil {
c.cache.Add(reqParams.Query, doc)
}
} }
op := doc.Operations.ForName(reqParams.OperationName) rc, err := c.exec.CreateOperationContext(ctx, params)
if op == nil {
c.sendError(message.ID, gqlerror.Errorf("operation %s not found", reqParams.OperationName))
return true
}
vars, err := validator.VariableValues(c.exec.Schema(), op, reqParams.Variables)
if err != nil { if err != nil {
c.sendError(message.ID, err) resp := c.exec.DispatchError(graphql.WithOperationContext(ctx, rc), err)
return true switch errcode.GetErrorKind(err) {
case errcode.KindProtocol:
c.sendError(message.ID, resp.Errors...)
default:
c.sendResponse(message.ID, &graphql.Response{Errors: err})
}
c.complete(message.ID)
return
} }
reqCtx := c.cfg.newRequestContext(c.exec, doc, op, reqParams.Query, vars)
ctx := graphql.WithRequestContext(c.ctx, reqCtx) ctx = graphql.WithOperationContext(ctx, rc)
if c.initPayload != nil { if c.initPayload != nil {
ctx = withInitPayload(ctx, c.initPayload) ctx = withInitPayload(ctx, c.initPayload)
} }
if op.Operation != ast.Subscription {
var result *graphql.Response
if op.Operation == ast.Query {
result = c.exec.Query(ctx, op)
} else {
result = c.exec.Mutation(ctx, op)
}
c.sendData(message.ID, result)
c.write(&operationMessage{ID: message.ID, Type: completeMsg})
return true
}
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
c.mu.Lock() c.mu.Lock()
c.active[message.ID] = cancel c.active[message.ID] = cancel
c.mu.Unlock() c.mu.Unlock()
go func() { go func() {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
userErr := reqCtx.Recover(ctx, r) userErr := rc.Recover(ctx, r)
c.sendError(message.ID, &gqlerror.Error{Message: userErr.Error()}) c.sendError(message.ID, &gqlerror.Error{Message: userErr.Error()})
} }
}() }()
next := c.exec.Subscription(ctx, op) responses, ctx := c.exec.DispatchOperation(ctx, rc)
for result := next(); result != nil; result = next() { for {
c.sendData(message.ID, result) response := responses(ctx)
} if response == nil {
break
}
c.write(&operationMessage{ID: message.ID, Type: completeMsg}) c.sendResponse(message.ID, response)
}
c.complete(message.ID)
c.mu.Lock() c.mu.Lock()
delete(c.active, message.ID) delete(c.active, message.ID)
c.mu.Unlock() c.mu.Unlock()
cancel() cancel()
}() }()
return true
} }
func (c *wsConnection) sendData(id string, response *graphql.Response) { func (c *wsConnection) sendResponse(id string, response *graphql.Response) {
b, err := json.Marshal(response) b, err := json.Marshal(response)
if err != nil { if err != nil {
c.sendError(id, gqlerror.Errorf("unable to encode json response: %s", err.Error())) panic(err)
return
} }
c.write(&operationMessage{
Payload: b,
ID: id,
Type: dataMsg,
})
}
c.write(&operationMessage{Type: dataMsg, ID: id, Payload: b}) func (c *wsConnection) complete(id string) {
c.write(&operationMessage{ID: id, Type: completeMsg})
} }
func (c *wsConnection) sendError(id string, errors ...*gqlerror.Error) { func (c *wsConnection) sendError(id string, errors ...*gqlerror.Error) {
var errs []error errs := make([]error, len(errors))
for _, err := range errors { for i, err := range errors {
errs = append(errs, err) errs[i] = err
} }
b, err := json.Marshal(errs) b, err := json.Marshal(errs)
if err != nil { if err != nil {
@@ -293,8 +293,10 @@ func (c *wsConnection) sendConnectionError(format string, args ...interface{}) {
func (c *wsConnection) readOp() *operationMessage { func (c *wsConnection) readOp() *operationMessage {
_, r, err := c.conn.NextReader() _, r, err := c.conn.NextReader()
if err != nil { if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) {
c.sendConnectionError("invalid json") return nil
} else if err != nil {
c.sendConnectionError("invalid json: %T %s", err, err.Error())
return nil return nil
} }
message := operationMessage{} message := operationMessage{}

View File

@@ -1,4 +1,4 @@
package handler package transport
import "context" import "context"
@@ -14,12 +14,12 @@ type InitPayload map[string]interface{}
// GetString safely gets a string value from the payload. It returns an empty string if the // GetString safely gets a string value from the payload. It returns an empty string if the
// payload is nil or the value isn't set. // payload is nil or the value isn't set.
func (payload InitPayload) GetString(key string) string { func (p InitPayload) GetString(key string) string {
if payload == nil { if p == nil {
return "" return ""
} }
if value, ok := payload[key]; ok { if value, ok := p[key]; ok {
res, _ := value.(string) res, _ := value.(string)
return res return res
} }
@@ -29,12 +29,12 @@ func (payload InitPayload) GetString(key string) string {
// Authorization is a short hand for getting the Authorization header from the // Authorization is a short hand for getting the Authorization header from the
// payload. // payload.
func (payload InitPayload) Authorization() string { func (p InitPayload) Authorization() string {
if value := payload.GetString("Authorization"); value != "" { if value := p.GetString("Authorization"); value != "" {
return value return value
} }
if value := payload.GetString("authorization"); value != "" { if value := p.GetString("authorization"); value != "" {
return value return value
} }

View File

@@ -20,6 +20,8 @@ func UnmarshalID(v interface{}) (string, error) {
return string(v), nil return string(v), nil
case int: case int:
return strconv.Itoa(v), nil return strconv.Itoa(v), nil
case int64:
return strconv.FormatInt(v, 10), nil
case float64: case float64:
return fmt.Sprintf("%f", v), nil return fmt.Sprintf("%f", v), nil
case bool: case bool:

View File

@@ -1,7 +1,7 @@
// introspection implements the spec defined in https://github.com/facebook/graphql/blob/master/spec/Section%204%20--%20Introspection.md#schema-introspection // introspection implements the spec defined in https://github.com/facebook/graphql/blob/master/spec/Section%204%20--%20Introspection.md#schema-introspection
package introspection package introspection
import "github.com/vektah/gqlparser/ast" import "github.com/vektah/gqlparser/v2/ast"
type ( type (
Directive struct { Directive struct {

View File

@@ -3,7 +3,7 @@ package introspection
import ( import (
"strings" "strings"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
) )
type Schema struct { type Schema struct {
@@ -11,7 +11,7 @@ type Schema struct {
} }
func (s *Schema) Types() []Type { func (s *Schema) Types() []Type {
var types []Type types := make([]Type, 0, len(s.schema.Types))
for _, typ := range s.schema.Types { for _, typ := range s.schema.Types {
if strings.HasPrefix(typ.Name, "__") { if strings.HasPrefix(typ.Name, "__") {
continue continue
@@ -34,7 +34,7 @@ func (s *Schema) SubscriptionType() *Type {
} }
func (s *Schema) Directives() []Directive { func (s *Schema) Directives() []Directive {
var res []Directive res := make([]Directive, 0, len(s.schema.Directives))
for _, d := range s.schema.Directives { for _, d := range s.schema.Directives {
res = append(res, s.directiveFromDef(d)) res = append(res, s.directiveFromDef(d))
@@ -44,19 +44,19 @@ func (s *Schema) Directives() []Directive {
} }
func (s *Schema) directiveFromDef(d *ast.DirectiveDefinition) Directive { func (s *Schema) directiveFromDef(d *ast.DirectiveDefinition) Directive {
var locs []string locs := make([]string, len(d.Locations))
for _, loc := range d.Locations { for i, loc := range d.Locations {
locs = append(locs, string(loc)) locs[i] = string(loc)
} }
var args []InputValue args := make([]InputValue, len(d.Arguments))
for _, arg := range d.Arguments { for i, arg := range d.Arguments {
args = append(args, InputValue{ args[i] = InputValue{
Name: arg.Name, Name: arg.Name,
Description: arg.Description, Description: arg.Description,
DefaultValue: defaultValue(arg.DefaultValue), DefaultValue: defaultValue(arg.DefaultValue),
Type: WrapTypeFromType(s.schema, arg.Type), Type: WrapTypeFromType(s.schema, arg.Type),
}) }
} }
return Directive{ return Directive{

View File

@@ -3,7 +3,7 @@ package introspection
import ( import (
"strings" "strings"
"github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/v2/ast"
) )
type Type struct { type Type struct {
@@ -152,6 +152,10 @@ func (t *Type) EnumValues(includeDeprecated bool) []EnumValue {
res := []EnumValue{} res := []EnumValue{}
for _, val := range t.def.EnumValues { for _, val := range t.def.EnumValues {
if !includeDeprecated && val.Directives.ForName("deprecated") != nil {
continue
}
res = append(res, EnumValue{ res = append(res, EnumValue{
Name: val.Name, Name: val.Name,
Description: val.Description, Description: val.Description,

View File

@@ -1,9 +1,11 @@
package graphql package graphql
func OneShot(resp *Response) func() *Response { import "context"
func OneShot(resp *Response) ResponseHandler {
var oneshot bool var oneshot bool
return func() *Response { return func(context context.Context) *Response {
if oneshot { if oneshot {
return nil return nil
} }

View File

@@ -1,4 +1,4 @@
package handler package playground
import ( import (
"html/template" "html/template"
@@ -11,7 +11,7 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
<meta charset=utf-8/> <meta charset=utf-8/>
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui"> <meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
<link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png"> <link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css" <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"
integrity="{{ .cssSRI }}" crossorigin="anonymous"/> integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png" <link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"
integrity="{{ .faviconSRI }}" crossorigin="anonymous"/> integrity="{{ .faviconSRI }}" crossorigin="anonymous"/>
@@ -33,6 +33,7 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
GraphQLPlayground.init(root, { GraphQLPlayground.init(root, {
endpoint: location.protocol + '//' + location.host + '{{.endpoint}}', endpoint: location.protocol + '//' + location.host + '{{.endpoint}}',
subscriptionsEndpoint: wsProto + '//' + location.host + '{{.endpoint }}', subscriptionsEndpoint: wsProto + '//' + location.host + '{{.endpoint }}',
shareEnabled: true,
settings: { settings: {
'request.credentials': 'same-origin' 'request.credentials': 'same-origin'
} }
@@ -43,7 +44,7 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
</html> </html>
`)) `))
func Playground(title string, endpoint string) http.HandlerFunc { func Handler(title string, endpoint string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html") w.Header().Add("Content-Type", "text/html")
err := page.Execute(w, map[string]string{ err := page.Execute(w, map[string]string{

View File

@@ -5,7 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/vektah/gqlparser/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
) )
// Errors are intentionally serialized first based on the advice in // Errors are intentionally serialized first based on the advice in

60
vendor/github.com/99designs/gqlgen/graphql/stats.go generated vendored Normal file
View File

@@ -0,0 +1,60 @@
package graphql
import (
"context"
"fmt"
"time"
)
type Stats struct {
OperationStart time.Time
Read TraceTiming
Parsing TraceTiming
Validation TraceTiming
// Stats collected by handler extensions. Dont use directly, the extension should provide a type safe way to
// access this.
extension map[string]interface{}
}
type TraceTiming struct {
Start time.Time
End time.Time
}
var ctxTraceStart key = "trace_start"
// StartOperationTrace captures the current time and stores it in context. This will eventually be added to request
// context but we want to grab it as soon as possible. For transports that can only handle a single graphql query
// per http requests you dont need to call this at all, the server will do it for you. For transports that handle
// multiple (eg batching, subscriptions) this should be called before decoding each request.
func StartOperationTrace(ctx context.Context) context.Context {
return context.WithValue(ctx, ctxTraceStart, Now())
}
// GetStartTime should only be called by the handler package, it will be set into request context
// as Stats.Start
func GetStartTime(ctx context.Context) time.Time {
t, ok := ctx.Value(ctxTraceStart).(time.Time)
if !ok {
panic(fmt.Sprintf("missing start time: %T", ctx.Value(ctxTraceStart)))
}
return t
}
func (c *Stats) SetExtension(name string, data interface{}) {
if c.extension == nil {
c.extension = map[string]interface{}{}
}
c.extension[name] = data
}
func (c *Stats) GetExtension(name string) interface{} {
if c.extension == nil {
return nil
}
return c.extension[name]
}
// Now is time.Now, except in tests. Then it can be whatever you want it to be.
var Now = time.Now

View File

@@ -1,58 +0,0 @@
package graphql
import (
"context"
)
var _ Tracer = (*NopTracer)(nil)
type Tracer interface {
StartOperationParsing(ctx context.Context) context.Context
EndOperationParsing(ctx context.Context)
StartOperationValidation(ctx context.Context) context.Context
EndOperationValidation(ctx context.Context)
StartOperationExecution(ctx context.Context) context.Context
StartFieldExecution(ctx context.Context, field CollectedField) context.Context
StartFieldResolverExecution(ctx context.Context, rc *ResolverContext) context.Context
StartFieldChildExecution(ctx context.Context) context.Context
EndFieldExecution(ctx context.Context)
EndOperationExecution(ctx context.Context)
}
type NopTracer struct{}
func (NopTracer) StartOperationParsing(ctx context.Context) context.Context {
return ctx
}
func (NopTracer) EndOperationParsing(ctx context.Context) {
}
func (NopTracer) StartOperationValidation(ctx context.Context) context.Context {
return ctx
}
func (NopTracer) EndOperationValidation(ctx context.Context) {
}
func (NopTracer) StartOperationExecution(ctx context.Context) context.Context {
return ctx
}
func (NopTracer) StartFieldExecution(ctx context.Context, field CollectedField) context.Context {
return ctx
}
func (NopTracer) StartFieldResolverExecution(ctx context.Context, rc *ResolverContext) context.Context {
return ctx
}
func (NopTracer) StartFieldChildExecution(ctx context.Context) context.Context {
return ctx
}
func (NopTracer) EndFieldExecution(ctx context.Context) {
}
func (NopTracer) EndOperationExecution(ctx context.Context) {
}

View File

@@ -6,9 +6,10 @@ import (
) )
type Upload struct { type Upload struct {
File io.Reader File io.Reader
Filename string Filename string
Size int64 Size int64
ContentType string
} }
func MarshalUpload(f Upload) Marshaler { func MarshalUpload(f Upload) Marshaler {

View File

@@ -1,3 +1,3 @@
package graphql package graphql
const Version = "v0.9.0" const Version = "v0.12.2"

View File

@@ -1,709 +0,0 @@
package handler
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/99designs/gqlgen/complexity"
"github.com/99designs/gqlgen/graphql"
"github.com/gorilla/websocket"
lru "github.com/hashicorp/golang-lru"
"github.com/vektah/gqlparser/ast"
"github.com/vektah/gqlparser/gqlerror"
"github.com/vektah/gqlparser/parser"
"github.com/vektah/gqlparser/validator"
)
type params struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}
type Config struct {
cacheSize int
upgrader websocket.Upgrader
recover graphql.RecoverFunc
errorPresenter graphql.ErrorPresenterFunc
resolverHook graphql.FieldMiddleware
requestHook graphql.RequestMiddleware
tracer graphql.Tracer
complexityLimit int
complexityLimitFunc graphql.ComplexityLimitFunc
disableIntrospection bool
connectionKeepAlivePingInterval time.Duration
uploadMaxMemory int64
uploadMaxSize int64
}
func (c *Config) newRequestContext(es graphql.ExecutableSchema, doc *ast.QueryDocument, op *ast.OperationDefinition, query string, variables map[string]interface{}) *graphql.RequestContext {
reqCtx := graphql.NewRequestContext(doc, query, variables)
reqCtx.DisableIntrospection = c.disableIntrospection
if hook := c.recover; hook != nil {
reqCtx.Recover = hook
}
if hook := c.errorPresenter; hook != nil {
reqCtx.ErrorPresenter = hook
}
if hook := c.resolverHook; hook != nil {
reqCtx.ResolverMiddleware = hook
}
if hook := c.requestHook; hook != nil {
reqCtx.RequestMiddleware = hook
}
if hook := c.tracer; hook != nil {
reqCtx.Tracer = hook
}
if c.complexityLimit > 0 || c.complexityLimitFunc != nil {
reqCtx.ComplexityLimit = c.complexityLimit
operationComplexity := complexity.Calculate(es, op, variables)
reqCtx.OperationComplexity = operationComplexity
}
return reqCtx
}
type Option func(cfg *Config)
func WebsocketUpgrader(upgrader websocket.Upgrader) Option {
return func(cfg *Config) {
cfg.upgrader = upgrader
}
}
func RecoverFunc(recover graphql.RecoverFunc) Option {
return func(cfg *Config) {
cfg.recover = recover
}
}
// ErrorPresenter transforms errors found while resolving into errors that will be returned to the user. It provides
// a good place to add any extra fields, like error.type, that might be desired by your frontend. Check the default
// implementation in graphql.DefaultErrorPresenter for an example.
func ErrorPresenter(f graphql.ErrorPresenterFunc) Option {
return func(cfg *Config) {
cfg.errorPresenter = f
}
}
// IntrospectionEnabled = false will forbid clients from calling introspection endpoints. Can be useful in prod when you dont
// want clients introspecting the full schema.
func IntrospectionEnabled(enabled bool) Option {
return func(cfg *Config) {
cfg.disableIntrospection = !enabled
}
}
// ComplexityLimit sets a maximum query complexity that is allowed to be executed.
// If a query is submitted that exceeds the limit, a 422 status code will be returned.
func ComplexityLimit(limit int) Option {
return func(cfg *Config) {
cfg.complexityLimit = limit
}
}
// ComplexityLimitFunc allows you to define a function to dynamically set the maximum query complexity that is allowed
// to be executed.
// If a query is submitted that exceeds the limit, a 422 status code will be returned.
func ComplexityLimitFunc(complexityLimitFunc graphql.ComplexityLimitFunc) Option {
return func(cfg *Config) {
cfg.complexityLimitFunc = complexityLimitFunc
}
}
// ResolverMiddleware allows you to define a function that will be called around every resolver,
// useful for logging.
func ResolverMiddleware(middleware graphql.FieldMiddleware) Option {
return func(cfg *Config) {
if cfg.resolverHook == nil {
cfg.resolverHook = middleware
return
}
lastResolve := cfg.resolverHook
cfg.resolverHook = func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {
return lastResolve(ctx, func(ctx context.Context) (res interface{}, err error) {
return middleware(ctx, next)
})
}
}
}
// RequestMiddleware allows you to define a function that will be called around the root request,
// after the query has been parsed. This is useful for logging
func RequestMiddleware(middleware graphql.RequestMiddleware) Option {
return func(cfg *Config) {
if cfg.requestHook == nil {
cfg.requestHook = middleware
return
}
lastResolve := cfg.requestHook
cfg.requestHook = func(ctx context.Context, next func(ctx context.Context) []byte) []byte {
return lastResolve(ctx, func(ctx context.Context) []byte {
return middleware(ctx, next)
})
}
}
}
// Tracer allows you to add a request/resolver tracer that will be called around the root request,
// calling resolver. This is useful for tracing
func Tracer(tracer graphql.Tracer) Option {
return func(cfg *Config) {
if cfg.tracer == nil {
cfg.tracer = tracer
} else {
lastResolve := cfg.tracer
cfg.tracer = &tracerWrapper{
tracer1: lastResolve,
tracer2: tracer,
}
}
opt := RequestMiddleware(func(ctx context.Context, next func(ctx context.Context) []byte) []byte {
ctx = tracer.StartOperationExecution(ctx)
resp := next(ctx)
tracer.EndOperationExecution(ctx)
return resp
})
opt(cfg)
}
}
type tracerWrapper struct {
tracer1 graphql.Tracer
tracer2 graphql.Tracer
}
func (tw *tracerWrapper) StartOperationParsing(ctx context.Context) context.Context {
ctx = tw.tracer1.StartOperationParsing(ctx)
ctx = tw.tracer2.StartOperationParsing(ctx)
return ctx
}
func (tw *tracerWrapper) EndOperationParsing(ctx context.Context) {
tw.tracer2.EndOperationParsing(ctx)
tw.tracer1.EndOperationParsing(ctx)
}
func (tw *tracerWrapper) StartOperationValidation(ctx context.Context) context.Context {
ctx = tw.tracer1.StartOperationValidation(ctx)
ctx = tw.tracer2.StartOperationValidation(ctx)
return ctx
}
func (tw *tracerWrapper) EndOperationValidation(ctx context.Context) {
tw.tracer2.EndOperationValidation(ctx)
tw.tracer1.EndOperationValidation(ctx)
}
func (tw *tracerWrapper) StartOperationExecution(ctx context.Context) context.Context {
ctx = tw.tracer1.StartOperationExecution(ctx)
ctx = tw.tracer2.StartOperationExecution(ctx)
return ctx
}
func (tw *tracerWrapper) StartFieldExecution(ctx context.Context, field graphql.CollectedField) context.Context {
ctx = tw.tracer1.StartFieldExecution(ctx, field)
ctx = tw.tracer2.StartFieldExecution(ctx, field)
return ctx
}
func (tw *tracerWrapper) StartFieldResolverExecution(ctx context.Context, rc *graphql.ResolverContext) context.Context {
ctx = tw.tracer1.StartFieldResolverExecution(ctx, rc)
ctx = tw.tracer2.StartFieldResolverExecution(ctx, rc)
return ctx
}
func (tw *tracerWrapper) StartFieldChildExecution(ctx context.Context) context.Context {
ctx = tw.tracer1.StartFieldChildExecution(ctx)
ctx = tw.tracer2.StartFieldChildExecution(ctx)
return ctx
}
func (tw *tracerWrapper) EndFieldExecution(ctx context.Context) {
tw.tracer2.EndFieldExecution(ctx)
tw.tracer1.EndFieldExecution(ctx)
}
func (tw *tracerWrapper) EndOperationExecution(ctx context.Context) {
tw.tracer2.EndOperationExecution(ctx)
tw.tracer1.EndOperationExecution(ctx)
}
// CacheSize sets the maximum size of the query cache.
// If size is less than or equal to 0, the cache is disabled.
func CacheSize(size int) Option {
return func(cfg *Config) {
cfg.cacheSize = size
}
}
// UploadMaxSize sets the maximum number of bytes used to parse a request body
// as multipart/form-data.
func UploadMaxSize(size int64) Option {
return func(cfg *Config) {
cfg.uploadMaxSize = size
}
}
// UploadMaxMemory sets the maximum number of bytes used to parse a request body
// as multipart/form-data in memory, with the remainder stored on disk in
// temporary files.
func UploadMaxMemory(size int64) Option {
return func(cfg *Config) {
cfg.uploadMaxMemory = size
}
}
// WebsocketKeepAliveDuration allows you to reconfigure the keepalive behavior.
// By default, keepalive is enabled with a DefaultConnectionKeepAlivePingInterval
// duration. Set handler.connectionKeepAlivePingInterval = 0 to disable keepalive
// altogether.
func WebsocketKeepAliveDuration(duration time.Duration) Option {
return func(cfg *Config) {
cfg.connectionKeepAlivePingInterval = duration
}
}
const DefaultCacheSize = 1000
const DefaultConnectionKeepAlivePingInterval = 25 * time.Second
// DefaultUploadMaxMemory is the maximum number of bytes used to parse a request body
// as multipart/form-data in memory, with the remainder stored on disk in
// temporary files.
const DefaultUploadMaxMemory = 32 << 20
// DefaultUploadMaxSize is maximum number of bytes used to parse a request body
// as multipart/form-data.
const DefaultUploadMaxSize = 32 << 20
func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc {
cfg := &Config{
cacheSize: DefaultCacheSize,
uploadMaxMemory: DefaultUploadMaxMemory,
uploadMaxSize: DefaultUploadMaxSize,
connectionKeepAlivePingInterval: DefaultConnectionKeepAlivePingInterval,
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
},
}
for _, option := range options {
option(cfg)
}
var cache *lru.Cache
if cfg.cacheSize > 0 {
var err error
cache, err = lru.New(cfg.cacheSize)
if err != nil {
// An error is only returned for non-positive cache size
// and we already checked for that.
panic("unexpected error creating cache: " + err.Error())
}
}
if cfg.tracer == nil {
cfg.tracer = &graphql.NopTracer{}
}
handler := &graphqlHandler{
cfg: cfg,
cache: cache,
exec: exec,
}
return handler.ServeHTTP
}
var _ http.Handler = (*graphqlHandler)(nil)
type graphqlHandler struct {
cfg *Config
cache *lru.Cache
exec graphql.ExecutableSchema
}
func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions {
w.Header().Set("Allow", "OPTIONS, GET, POST")
w.WriteHeader(http.StatusOK)
return
}
if strings.Contains(r.Header.Get("Upgrade"), "websocket") {
connectWs(gh.exec, w, r, gh.cfg, gh.cache)
return
}
w.Header().Set("Content-Type", "application/json")
var reqParams params
switch r.Method {
case http.MethodGet:
reqParams.Query = r.URL.Query().Get("query")
reqParams.OperationName = r.URL.Query().Get("operationName")
if variables := r.URL.Query().Get("variables"); variables != "" {
if err := jsonDecode(strings.NewReader(variables), &reqParams.Variables); err != nil {
sendErrorf(w, http.StatusBadRequest, "variables could not be decoded")
return
}
}
case http.MethodPost:
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
sendErrorf(w, http.StatusBadRequest, "error parsing request Content-Type")
return
}
switch mediaType {
case "application/json":
if err := jsonDecode(r.Body, &reqParams); err != nil {
sendErrorf(w, http.StatusBadRequest, "json body could not be decoded: "+err.Error())
return
}
case "multipart/form-data":
var closers []io.Closer
var tmpFiles []string
defer func() {
for i := len(closers) - 1; 0 <= i; i-- {
_ = closers[i].Close()
}
for _, tmpFile := range tmpFiles {
_ = os.Remove(tmpFile)
}
}()
if err := processMultipart(w, r, &reqParams, &closers, &tmpFiles, gh.cfg.uploadMaxSize, gh.cfg.uploadMaxMemory); err != nil {
sendErrorf(w, http.StatusBadRequest, "multipart body could not be decoded: "+err.Error())
return
}
default:
sendErrorf(w, http.StatusBadRequest, "unsupported Content-Type: "+mediaType)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
ctx := r.Context()
var doc *ast.QueryDocument
var cacheHit bool
if gh.cache != nil {
val, ok := gh.cache.Get(reqParams.Query)
if ok {
doc = val.(*ast.QueryDocument)
cacheHit = true
}
}
ctx, doc, gqlErr := gh.parseOperation(ctx, &parseOperationArgs{
Query: reqParams.Query,
CachedDoc: doc,
})
if gqlErr != nil {
sendError(w, http.StatusUnprocessableEntity, gqlErr)
return
}
ctx, op, vars, listErr := gh.validateOperation(ctx, &validateOperationArgs{
Doc: doc,
OperationName: reqParams.OperationName,
CacheHit: cacheHit,
R: r,
Variables: reqParams.Variables,
})
if len(listErr) != 0 {
sendError(w, http.StatusUnprocessableEntity, listErr...)
return
}
if gh.cache != nil && !cacheHit {
gh.cache.Add(reqParams.Query, doc)
}
reqCtx := gh.cfg.newRequestContext(gh.exec, doc, op, reqParams.Query, vars)
ctx = graphql.WithRequestContext(ctx, reqCtx)
defer func() {
if err := recover(); err != nil {
userErr := reqCtx.Recover(ctx, err)
sendErrorf(w, http.StatusUnprocessableEntity, userErr.Error())
}
}()
if gh.cfg.complexityLimitFunc != nil {
reqCtx.ComplexityLimit = gh.cfg.complexityLimitFunc(ctx)
}
if reqCtx.ComplexityLimit > 0 && reqCtx.OperationComplexity > reqCtx.ComplexityLimit {
sendErrorf(w, http.StatusUnprocessableEntity, "operation has complexity %d, which exceeds the limit of %d", reqCtx.OperationComplexity, reqCtx.ComplexityLimit)
return
}
switch op.Operation {
case ast.Query:
b, err := json.Marshal(gh.exec.Query(ctx, op))
if err != nil {
panic(err)
}
w.Write(b)
case ast.Mutation:
b, err := json.Marshal(gh.exec.Mutation(ctx, op))
if err != nil {
panic(err)
}
w.Write(b)
default:
sendErrorf(w, http.StatusBadRequest, "unsupported operation type")
}
}
type parseOperationArgs struct {
Query string
CachedDoc *ast.QueryDocument
}
func (gh *graphqlHandler) parseOperation(ctx context.Context, args *parseOperationArgs) (context.Context, *ast.QueryDocument, *gqlerror.Error) {
ctx = gh.cfg.tracer.StartOperationParsing(ctx)
defer func() { gh.cfg.tracer.EndOperationParsing(ctx) }()
if args.CachedDoc != nil {
return ctx, args.CachedDoc, nil
}
doc, gqlErr := parser.ParseQuery(&ast.Source{Input: args.Query})
if gqlErr != nil {
return ctx, nil, gqlErr
}
return ctx, doc, nil
}
type validateOperationArgs struct {
Doc *ast.QueryDocument
OperationName string
CacheHit bool
R *http.Request
Variables map[string]interface{}
}
func (gh *graphqlHandler) validateOperation(ctx context.Context, args *validateOperationArgs) (context.Context, *ast.OperationDefinition, map[string]interface{}, gqlerror.List) {
ctx = gh.cfg.tracer.StartOperationValidation(ctx)
defer func() { gh.cfg.tracer.EndOperationValidation(ctx) }()
if !args.CacheHit {
listErr := validator.Validate(gh.exec.Schema(), args.Doc)
if len(listErr) != 0 {
return ctx, nil, nil, listErr
}
}
op := args.Doc.Operations.ForName(args.OperationName)
if op == nil {
return ctx, nil, nil, gqlerror.List{gqlerror.Errorf("operation %s not found", args.OperationName)}
}
if op.Operation != ast.Query && args.R.Method == http.MethodGet {
return ctx, nil, nil, gqlerror.List{gqlerror.Errorf("GET requests only allow query operations")}
}
vars, err := validator.VariableValues(gh.exec.Schema(), op, args.Variables)
if err != nil {
return ctx, nil, nil, gqlerror.List{err}
}
return ctx, op, vars, nil
}
func jsonDecode(r io.Reader, val interface{}) error {
dec := json.NewDecoder(r)
dec.UseNumber()
return dec.Decode(val)
}
func sendError(w http.ResponseWriter, code int, errors ...*gqlerror.Error) {
w.WriteHeader(code)
b, err := json.Marshal(&graphql.Response{Errors: errors})
if err != nil {
panic(err)
}
w.Write(b)
}
func sendErrorf(w http.ResponseWriter, code int, format string, args ...interface{}) {
sendError(w, code, &gqlerror.Error{Message: fmt.Sprintf(format, args...)})
}
type bytesReader struct {
s *[]byte
i int64 // current reading index
prevRune int // index of previous rune; or < 0
}
func (r *bytesReader) Read(b []byte) (n int, err error) {
if r.s == nil {
return 0, errors.New("byte slice pointer is nil")
}
if r.i >= int64(len(*r.s)) {
return 0, io.EOF
}
r.prevRune = -1
n = copy(b, (*r.s)[r.i:])
r.i += int64(n)
return
}
func processMultipart(w http.ResponseWriter, r *http.Request, request *params, closers *[]io.Closer, tmpFiles *[]string, uploadMaxSize, uploadMaxMemory int64) error {
var err error
if r.ContentLength > uploadMaxSize {
return errors.New("failed to parse multipart form, request body too large")
}
r.Body = http.MaxBytesReader(w, r.Body, uploadMaxSize)
if err = r.ParseMultipartForm(uploadMaxMemory); err != nil {
if strings.Contains(err.Error(), "request body too large") {
return errors.New("failed to parse multipart form, request body too large")
}
return errors.New("failed to parse multipart form")
}
*closers = append(*closers, r.Body)
if err = jsonDecode(strings.NewReader(r.Form.Get("operations")), &request); err != nil {
return errors.New("operations form field could not be decoded")
}
var uploadsMap = map[string][]string{}
if err = json.Unmarshal([]byte(r.Form.Get("map")), &uploadsMap); err != nil {
return errors.New("map form field could not be decoded")
}
var upload graphql.Upload
for key, paths := range uploadsMap {
if len(paths) == 0 {
return fmt.Errorf("invalid empty operations paths list for key %s", key)
}
file, header, err := r.FormFile(key)
if err != nil {
return fmt.Errorf("failed to get key %s from form", key)
}
*closers = append(*closers, file)
if len(paths) == 1 {
upload = graphql.Upload{
File: file,
Size: header.Size,
Filename: header.Filename,
}
err = addUploadToOperations(request, upload, key, paths[0])
if err != nil {
return err
}
} else {
if r.ContentLength < uploadMaxMemory {
fileBytes, err := ioutil.ReadAll(file)
if err != nil {
return fmt.Errorf("failed to read file for key %s", key)
}
for _, path := range paths {
upload = graphql.Upload{
File: &bytesReader{s: &fileBytes, i: 0, prevRune: -1},
Size: header.Size,
Filename: header.Filename,
}
err = addUploadToOperations(request, upload, key, path)
if err != nil {
return err
}
}
} else {
tmpFile, err := ioutil.TempFile(os.TempDir(), "gqlgen-")
if err != nil {
return fmt.Errorf("failed to create temp file for key %s", key)
}
tmpName := tmpFile.Name()
*tmpFiles = append(*tmpFiles, tmpName)
_, err = io.Copy(tmpFile, file)
if err != nil {
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to copy to temp file and close temp file for key %s", key)
}
return fmt.Errorf("failed to copy to temp file for key %s", key)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp file for key %s", key)
}
for _, path := range paths {
pathTmpFile, err := os.Open(tmpName)
if err != nil {
return fmt.Errorf("failed to open temp file for key %s", key)
}
*closers = append(*closers, pathTmpFile)
upload = graphql.Upload{
File: pathTmpFile,
Size: header.Size,
Filename: header.Filename,
}
err = addUploadToOperations(request, upload, key, path)
if err != nil {
return err
}
}
}
}
}
return nil
}
func addUploadToOperations(request *params, upload graphql.Upload, key, path string) error {
if !strings.HasPrefix(path, "variables.") {
return fmt.Errorf("invalid operations paths for key %s", key)
}
var ptr interface{} = request.Variables
parts := strings.Split(path, ".")
// skip the first part (variables) because we started there
for i, p := range parts[1:] {
last := i == len(parts)-2
if ptr == nil {
return fmt.Errorf("path is missing \"variables.\" prefix, key: %s, path: %s", key, path)
}
if index, parseNbrErr := strconv.Atoi(p); parseNbrErr == nil {
if last {
ptr.([]interface{})[index] = upload
} else {
ptr = ptr.([]interface{})[index]
}
} else {
if last {
ptr.(map[string]interface{})[p] = upload
} else {
ptr = ptr.(map[string]interface{})[p]
}
}
}
return nil
}

247
vendor/github.com/99designs/gqlgen/handler/handler.go generated vendored Normal file
View File

@@ -0,0 +1,247 @@
package handler
import (
"context"
"net/http"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/gorilla/websocket"
)
// Deprecated: switch to graphql/handler.New
func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc {
var cfg Config
cfg.cacheSize = 1000
for _, option := range options {
option(&cfg)
}
srv := handler.New(exec)
srv.AddTransport(transport.Websocket{
Upgrader: cfg.upgrader,
InitFunc: cfg.websocketInitFunc,
KeepAlivePingInterval: cfg.connectionKeepAlivePingInterval,
})
srv.AddTransport(transport.Options{})
srv.AddTransport(transport.GET{})
srv.AddTransport(transport.POST{})
srv.AddTransport(transport.MultipartForm{
MaxUploadSize: cfg.uploadMaxSize,
MaxMemory: cfg.uploadMaxMemory,
})
if cfg.cacheSize != 0 {
srv.SetQueryCache(lru.New(cfg.cacheSize))
}
if cfg.recover != nil {
srv.SetRecoverFunc(cfg.recover)
}
if cfg.errorPresenter != nil {
srv.SetErrorPresenter(cfg.errorPresenter)
}
for _, hook := range cfg.fieldHooks {
srv.AroundFields(hook)
}
for _, hook := range cfg.requestHooks {
srv.AroundResponses(hook)
}
if cfg.complexityLimit != 0 {
srv.Use(extension.FixedComplexityLimit(cfg.complexityLimit))
} else if cfg.complexityLimitFunc != nil {
srv.Use(&extension.ComplexityLimit{
Func: func(ctx context.Context, rc *graphql.OperationContext) int {
return cfg.complexityLimitFunc(graphql.WithOperationContext(ctx, rc))
},
})
}
if !cfg.disableIntrospection {
srv.Use(extension.Introspection{})
}
if cfg.apqCache != nil {
srv.Use(extension.AutomaticPersistedQuery{Cache: apqAdapter{cfg.apqCache}})
}
return srv.ServeHTTP
}
// Deprecated: switch to graphql/handler.New
type Config struct {
cacheSize int
upgrader websocket.Upgrader
websocketInitFunc transport.WebsocketInitFunc
connectionKeepAlivePingInterval time.Duration
recover graphql.RecoverFunc
errorPresenter graphql.ErrorPresenterFunc
fieldHooks []graphql.FieldMiddleware
requestHooks []graphql.ResponseMiddleware
complexityLimit int
complexityLimitFunc func(ctx context.Context) int
disableIntrospection bool
uploadMaxMemory int64
uploadMaxSize int64
apqCache PersistedQueryCache
}
// Deprecated: switch to graphql/handler.New
type Option func(cfg *Config)
// Deprecated: switch to graphql/handler.New
func WebsocketUpgrader(upgrader websocket.Upgrader) Option {
return func(cfg *Config) {
cfg.upgrader = upgrader
}
}
// Deprecated: switch to graphql/handler.New
func RecoverFunc(recover graphql.RecoverFunc) Option {
return func(cfg *Config) {
cfg.recover = recover
}
}
// ErrorPresenter transforms errors found while resolving into errors that will be returned to the user. It provides
// a good place to add any extra fields, like error.type, that might be desired by your frontend. Check the default
// implementation in graphql.DefaultErrorPresenter for an example.
// Deprecated: switch to graphql/handler.New
func ErrorPresenter(f graphql.ErrorPresenterFunc) Option {
return func(cfg *Config) {
cfg.errorPresenter = f
}
}
// IntrospectionEnabled = false will forbid clients from calling introspection endpoints. Can be useful in prod when you dont
// want clients introspecting the full schema.
// Deprecated: switch to graphql/handler.New
func IntrospectionEnabled(enabled bool) Option {
return func(cfg *Config) {
cfg.disableIntrospection = !enabled
}
}
// ComplexityLimit sets a maximum query complexity that is allowed to be executed.
// If a query is submitted that exceeds the limit, a 422 status code will be returned.
// Deprecated: switch to graphql/handler.New
func ComplexityLimit(limit int) Option {
return func(cfg *Config) {
cfg.complexityLimit = limit
}
}
// ComplexityLimitFunc allows you to define a function to dynamically set the maximum query complexity that is allowed
// to be executed.
// If a query is submitted that exceeds the limit, a 422 status code will be returned.
// Deprecated: switch to graphql/handler.New
func ComplexityLimitFunc(complexityLimitFunc func(ctx context.Context) int) Option {
return func(cfg *Config) {
cfg.complexityLimitFunc = complexityLimitFunc
}
}
// ResolverMiddleware allows you to define a function that will be called around every resolver,
// useful for logging.
// Deprecated: switch to graphql/handler.New
func ResolverMiddleware(middleware graphql.FieldMiddleware) Option {
return func(cfg *Config) {
cfg.fieldHooks = append(cfg.fieldHooks, middleware)
}
}
// RequestMiddleware allows you to define a function that will be called around the root request,
// after the query has been parsed. This is useful for logging
// Deprecated: switch to graphql/handler.New
func RequestMiddleware(middleware graphql.ResponseMiddleware) Option {
return func(cfg *Config) {
cfg.requestHooks = append(cfg.requestHooks, middleware)
}
}
// WebsocketInitFunc is called when the server receives connection init message from the client.
// This can be used to check initial payload to see whether to accept the websocket connection.
// Deprecated: switch to graphql/handler.New
func WebsocketInitFunc(websocketInitFunc transport.WebsocketInitFunc) Option {
return func(cfg *Config) {
cfg.websocketInitFunc = websocketInitFunc
}
}
// CacheSize sets the maximum size of the query cache.
// If size is less than or equal to 0, the cache is disabled.
// Deprecated: switch to graphql/handler.New
func CacheSize(size int) Option {
return func(cfg *Config) {
cfg.cacheSize = size
}
}
// UploadMaxSize sets the maximum number of bytes used to parse a request body
// as multipart/form-data.
// Deprecated: switch to graphql/handler.New
func UploadMaxSize(size int64) Option {
return func(cfg *Config) {
cfg.uploadMaxSize = size
}
}
// UploadMaxMemory sets the maximum number of bytes used to parse a request body
// as multipart/form-data in memory, with the remainder stored on disk in
// temporary files.
// Deprecated: switch to graphql/handler.New
func UploadMaxMemory(size int64) Option {
return func(cfg *Config) {
cfg.uploadMaxMemory = size
}
}
// WebsocketKeepAliveDuration allows you to reconfigure the keepalive behavior.
// By default, keepalive is enabled with a DefaultConnectionKeepAlivePingInterval
// duration. Set handler.connectionKeepAlivePingInterval = 0 to disable keepalive
// altogether.
// Deprecated: switch to graphql/handler.New
func WebsocketKeepAliveDuration(duration time.Duration) Option {
return func(cfg *Config) {
cfg.connectionKeepAlivePingInterval = duration
}
}
// Add cache that will hold queries for automatic persisted queries (APQ)
// Deprecated: switch to graphql/handler.New
func EnablePersistedQueryCache(cache PersistedQueryCache) Option {
return func(cfg *Config) {
cfg.apqCache = cache
}
}
func GetInitPayload(ctx context.Context) transport.InitPayload {
return transport.GetInitPayload(ctx)
}
type apqAdapter struct {
PersistedQueryCache
}
func (a apqAdapter) Get(ctx context.Context, key string) (value interface{}, ok bool) {
return a.PersistedQueryCache.Get(ctx, key)
}
func (a apqAdapter) Add(ctx context.Context, key string, value interface{}) {
a.PersistedQueryCache.Add(ctx, key, value.(string))
}
type PersistedQueryCache interface {
Add(ctx context.Context, hash string, query string)
Get(ctx context.Context, hash string) (string, bool)
}
// Deprecated: use playground.Handler instead
func Playground(title string, endpoint string) http.HandlerFunc {
return playground.Handler(title, endpoint)
}
// Deprecated: use transport.InitPayload instead
type InitPayload = transport.InitPayload

View File

@@ -1,57 +0,0 @@
package handler
import (
"context"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser"
"github.com/vektah/gqlparser/ast"
)
type executableSchemaMock struct {
MutationFunc func(ctx context.Context, op *ast.OperationDefinition) *graphql.Response
}
var _ graphql.ExecutableSchema = &executableSchemaMock{}
func (e *executableSchemaMock) Schema() *ast.Schema {
return gqlparser.MustLoadSchema(&ast.Source{Input: `
schema { query: Query, mutation: Mutation }
type Query {
empty: String!
}
scalar Upload
type File {
id: Int!
}
input UploadFile {
id: Int!
file: Upload!
}
type Mutation {
singleUpload(file: Upload!): File!
singleUploadWithPayload(req: UploadFile!): File!
multipleUpload(files: [Upload!]!): [File!]!
multipleUploadWithPayload(req: [UploadFile!]!): [File!]!
}
`})
}
func (e *executableSchemaMock) Complexity(typeName, field string, childComplexity int, args map[string]interface{}) (int, bool) {
return 0, false
}
func (e *executableSchemaMock) Query(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
return graphql.ErrorResponse(ctx, "queries are not supported")
}
func (e *executableSchemaMock) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
return e.MutationFunc(ctx, op)
}
func (e *executableSchemaMock) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response {
return func() *graphql.Response {
<-ctx.Done()
return nil
}
}

View File

@@ -1,51 +0,0 @@
package handler
import (
"context"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser"
"github.com/vektah/gqlparser/ast"
)
type executableSchemaStub struct {
NextResp chan struct{}
}
var _ graphql.ExecutableSchema = &executableSchemaStub{}
func (e *executableSchemaStub) Schema() *ast.Schema {
return gqlparser.MustLoadSchema(&ast.Source{Input: `
schema { query: Query }
type Query {
me: User!
user(id: Int): User!
}
type User { name: String! }
`})
}
func (e *executableSchemaStub) Complexity(typeName, field string, childComplexity int, args map[string]interface{}) (int, bool) {
return 0, false
}
func (e *executableSchemaStub) Query(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
return &graphql.Response{Data: []byte(`{"name":"test"}`)}
}
func (e *executableSchemaStub) Mutation(ctx context.Context, op *ast.OperationDefinition) *graphql.Response {
return graphql.ErrorResponse(ctx, "mutations are not supported")
}
func (e *executableSchemaStub) Subscription(ctx context.Context, op *ast.OperationDefinition) func() *graphql.Response {
return func() *graphql.Response {
select {
case <-ctx.Done():
return nil
case <-e.NextResp:
return &graphql.Response{
Data: []byte(`{"name":"test"}`),
}
}
}
}

View File

@@ -1,7 +1,6 @@
package code package code
import ( import (
"errors"
"go/build" "go/build"
"go/parser" "go/parser"
"go/token" "go/token"
@@ -9,13 +8,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"sync"
"golang.org/x/tools/go/packages"
) )
var nameForPackageCache = sync.Map{}
var gopaths []string var gopaths []string
func init() { func init() {
@@ -51,33 +45,50 @@ func NameForDir(dir string) string {
return SanitizePackageName(filepath.Base(dir)) return SanitizePackageName(filepath.Base(dir))
} }
// ImportPathForDir takes a path and returns a golang import path for the package // goModuleRoot returns the root of the current go module if there is a go.mod file in the directory tree
func ImportPathForDir(dir string) (res string) { // If not, it returns false
func goModuleRoot(dir string) (string, bool) {
dir, err := filepath.Abs(dir) dir, err := filepath.Abs(dir)
if err != nil { if err != nil {
panic(err) panic(err)
} }
dir = filepath.ToSlash(dir) dir = filepath.ToSlash(dir)
modDir := dir modDir := dir
assumedPart := "" assumedPart := ""
for { for {
f, err := ioutil.ReadFile(filepath.Join(modDir, "/", "go.mod")) f, err := ioutil.ReadFile(filepath.Join(modDir, "go.mod"))
if err == nil { if err == nil {
// found it, stop searching // found it, stop searching
return string(modregex.FindSubmatch(f)[1]) + assumedPart return string(modregex.FindSubmatch(f)[1]) + assumedPart, true
} }
assumedPart = "/" + filepath.Base(modDir) + assumedPart assumedPart = "/" + filepath.Base(modDir) + assumedPart
modDir, err = filepath.Abs(filepath.Join(modDir, "..")) parentDir, err := filepath.Abs(filepath.Join(modDir, ".."))
if err != nil { if err != nil {
panic(err) panic(err)
} }
// Walked all the way to the root and didnt find anything :'( if parentDir == modDir {
if modDir == "/" { // Walked all the way to the root and didnt find anything :'(
break break
} }
modDir = parentDir
}
return "", false
}
// ImportPathForDir takes a path and returns a golang import path for the package
func ImportPathForDir(dir string) (res string) {
dir, err := filepath.Abs(dir)
if err != nil {
panic(err)
}
dir = filepath.ToSlash(dir)
modDir, ok := goModuleRoot(dir)
if ok {
return modDir
} }
for _, gopath := range gopaths { for _, gopath := range gopaths {
@@ -89,26 +100,4 @@ func ImportPathForDir(dir string) (res string) {
return "" return ""
} }
var modregex = regexp.MustCompile("module (.*)\n") var modregex = regexp.MustCompile(`module ([^\s]*)`)
// NameForPackage returns the package name for a given import path. This can be really slow.
func NameForPackage(importPath string) string {
if importPath == "" {
panic(errors.New("import path can not be empty"))
}
if v, ok := nameForPackageCache.Load(importPath); ok {
return v.(string)
}
importPath = QualifyPackagePath(importPath)
p, _ := packages.Load(&packages.Config{
Mode: packages.NeedName,
}, importPath)
if len(p) != 1 || p[0].Name == "" {
return SanitizePackageName(filepath.Base(importPath))
}
nameForPackageCache.Store(importPath, p[0].Name)
return p[0].Name
}

View File

@@ -0,0 +1,173 @@
package code
import (
"bytes"
"path/filepath"
"github.com/pkg/errors"
"golang.org/x/tools/go/packages"
)
var mode = packages.NeedName |
packages.NeedFiles |
packages.NeedImports |
packages.NeedTypes |
packages.NeedSyntax |
packages.NeedTypesInfo
// Packages is a wrapper around x/tools/go/packages that maintains a (hopefully prewarmed) cache of packages
// that can be invalidated as writes are made and packages are known to change.
type Packages struct {
packages map[string]*packages.Package
importToName map[string]string
loadErrors []error
numLoadCalls int // stupid test steam. ignore.
numNameCalls int // stupid test steam. ignore.
}
// LoadAll will call packages.Load and return the package data for the given packages,
// but if the package already have been loaded it will return cached values instead.
func (p *Packages) LoadAll(importPaths ...string) []*packages.Package {
if p.packages == nil {
p.packages = map[string]*packages.Package{}
}
missing := make([]string, 0, len(importPaths))
for _, path := range importPaths {
if _, ok := p.packages[path]; ok {
continue
}
missing = append(missing, path)
}
if len(missing) > 0 {
p.numLoadCalls++
pkgs, err := packages.Load(&packages.Config{Mode: mode}, missing...)
if err != nil {
p.loadErrors = append(p.loadErrors, err)
}
for _, pkg := range pkgs {
p.addToCache(pkg)
}
}
res := make([]*packages.Package, 0, len(importPaths))
for _, path := range importPaths {
res = append(res, p.packages[NormalizeVendor(path)])
}
return res
}
func (p *Packages) addToCache(pkg *packages.Package) {
imp := NormalizeVendor(pkg.PkgPath)
p.packages[imp] = pkg
for _, imp := range pkg.Imports {
if _, found := p.packages[NormalizeVendor(imp.PkgPath)]; !found {
p.addToCache(imp)
}
}
}
// Load works the same as LoadAll, except a single package at a time.
func (p *Packages) Load(importPath string) *packages.Package {
pkgs := p.LoadAll(importPath)
if len(pkgs) == 0 {
return nil
}
return pkgs[0]
}
// LoadWithTypes tries a standard load, which may not have enough type info (TypesInfo== nil) available if the imported package is a
// second order dependency. Fortunately this doesnt happen very often, so we can just issue a load when we detect it.
func (p *Packages) LoadWithTypes(importPath string) *packages.Package {
pkg := p.Load(importPath)
if pkg == nil || pkg.TypesInfo == nil {
p.numLoadCalls++
pkgs, err := packages.Load(&packages.Config{Mode: mode}, importPath)
if err != nil {
p.loadErrors = append(p.loadErrors, err)
return nil
}
p.addToCache(pkgs[0])
pkg = pkgs[0]
}
return pkg
}
// NameForPackage looks up the package name from the package stanza in the go files at the given import path.
func (p *Packages) NameForPackage(importPath string) string {
if importPath == "" {
panic(errors.New("import path can not be empty"))
}
if p.importToName == nil {
p.importToName = map[string]string{}
}
importPath = NormalizeVendor(importPath)
// if its in the name cache use it
if name := p.importToName[importPath]; name != "" {
return name
}
// otherwise we might have already loaded the full package data for it cached
pkg := p.packages[importPath]
if pkg == nil {
// otherwise do a name only lookup for it but dont put it in the package cache.
p.numNameCalls++
pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName}, importPath)
if err != nil {
p.loadErrors = append(p.loadErrors, err)
} else {
pkg = pkgs[0]
}
}
if pkg == nil || pkg.Name == "" {
return SanitizePackageName(filepath.Base(importPath))
}
p.importToName[importPath] = pkg.Name
return pkg.Name
}
// Evict removes a given package import path from the cache, along with any packages that depend on it. Further calls
// to Load will fetch it from disk.
func (p *Packages) Evict(importPath string) {
delete(p.packages, importPath)
for _, pkg := range p.packages {
for _, imported := range pkg.Imports {
if imported.PkgPath == importPath {
p.Evict(pkg.PkgPath)
}
}
}
}
// Errors returns any errors that were returned by Load, either from the call itself or any of the loaded packages.
func (p *Packages) Errors() PkgErrors {
var res []error //nolint:prealloc
res = append(res, p.loadErrors...)
for _, pkg := range p.packages {
for _, err := range pkg.Errors {
res = append(res, err)
}
}
return res
}
type PkgErrors []error
func (p PkgErrors) Error() string {
var b bytes.Buffer
b.WriteString("packages.Load: ")
for _, e := range p {
b.WriteString(e.Error() + "\n")
}
return b.String()
}

View File

@@ -41,6 +41,11 @@ func NormalizeVendor(pkg string) string {
func QualifyPackagePath(importPath string) string { func QualifyPackagePath(importPath string) string {
wd, _ := os.Getwd() wd, _ := os.Getwd()
// in go module mode, the import path doesn't need fixing
if _, ok := goModuleRoot(wd); ok {
return importPath
}
pkg, err := build.Import(importPath, wd, 0) pkg, err := build.Import(importPath, wd, 0)
if err != nil { if err != nil {
return importPath return importPath

View File

@@ -24,7 +24,7 @@ func (fn visitFn) Visit(node ast.Node) ast.Visitor {
} }
// Prune removes any unused imports // Prune removes any unused imports
func Prune(filename string, src []byte) ([]byte, error) { func Prune(filename string, src []byte, packages *code.Packages) ([]byte, error) {
fset := token.NewFileSet() fset := token.NewFileSet()
file, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.AllErrors) file, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.AllErrors)
@@ -32,10 +32,7 @@ func Prune(filename string, src []byte) ([]byte, error) {
return nil, err return nil, err
} }
unused, err := getUnusedImports(file, filename) unused := getUnusedImports(file, packages)
if err != nil {
return nil, err
}
for ipath, name := range unused { for ipath, name := range unused {
astutil.DeleteNamedImport(fset, file, name, ipath) astutil.DeleteNamedImport(fset, file, name, ipath)
} }
@@ -49,7 +46,7 @@ func Prune(filename string, src []byte) ([]byte, error) {
return imports.Process(filename, buf.Bytes(), &imports.Options{FormatOnly: true, Comments: true, TabIndent: true, TabWidth: 8}) return imports.Process(filename, buf.Bytes(), &imports.Options{FormatOnly: true, Comments: true, TabIndent: true, TabWidth: 8})
} }
func getUnusedImports(file ast.Node, filename string) (map[string]string, error) { func getUnusedImports(file ast.Node, packages *code.Packages) map[string]string {
imported := map[string]*ast.ImportSpec{} imported := map[string]*ast.ImportSpec{}
used := map[string]bool{} used := map[string]bool{}
@@ -68,7 +65,7 @@ func getUnusedImports(file ast.Node, filename string) (map[string]string, error)
break break
} }
local := code.NameForPackage(ipath) local := packages.NameForPackage(ipath)
imported[local] = v imported[local] = v
case *ast.SelectorExpr: case *ast.SelectorExpr:
@@ -99,5 +96,5 @@ func getUnusedImports(file ast.Node, filename string) (map[string]string, error)
} }
} }
return unusedImport, nil return unusedImport
} }

Some files were not shown because too many files have changed in this diff Show More