GraphQL Framework

This commit is contained in:
Evan Reichard 2021-02-02 15:34:10 -05:00
parent ecf981495e
commit 7e6454c593
28 changed files with 10764 additions and 616 deletions

View File

@ -7,6 +7,10 @@ import (
log "github.com/sirupsen/logrus"
"reichard.io/imagini/cmd/server"
"reichard.io/imagini/plugin"
"github.com/99designs/gqlgen/api"
"github.com/99designs/gqlgen/codegen/config"
)
type UTCFormatter struct {
@ -21,7 +25,6 @@ func (u UTCFormatter) Format(e *log.Entry) ([]byte, error) {
func main() {
log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}})
log.Info("Starting Imagini")
app := &cli.App{
Name: "Imagini",
Usage: "A self hosted photo library.",
@ -32,6 +35,11 @@ func main() {
Usage: "Start Imagini web server.",
Action: cmdServer,
},
{
Name: "generate",
Usage: "generate graphql schema",
Action: cmdGenerate,
},
},
}
err := app.Run(os.Args)
@ -41,6 +49,7 @@ func main() {
}
func cmdServer(ctx *cli.Context) error {
log.Info("Starting Imagini Server")
server := server.NewServer()
server.StartServer()
@ -53,3 +62,24 @@ func cmdServer(ctx *cli.Context) error {
return nil
}
func cmdGenerate(ctx *cli.Context) error {
log.Info("Generating Imagini Models")
gqlgenConf, err := config.LoadConfigFromDefaultLocations()
if err != nil {
log.Panic("Failed to load config", err.Error())
os.Exit(2)
}
log.Info("Generating Schema...")
err = api.Generate(gqlgenConf,
api.AddPlugin(plugin.New()),
)
log.Info("Schema Generation Done")
if err != nil {
log.Panic(err.Error())
os.Exit(3)
}
os.Exit(0)
return nil
}

2
go.mod
View File

@ -3,6 +3,7 @@ module reichard.io/imagini
go 1.15
require (
github.com/99designs/gqlgen v0.13.0
github.com/codeon/govips v0.0.0-20200329201227-415341c0ce33 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2 // indirect
@ -14,6 +15,7 @@ require (
github.com/sirupsen/logrus v1.7.0
github.com/tus/tusd v1.4.0
github.com/urfave/cli/v2 v2.3.0
github.com/vektah/gqlparser/v2 v2.1.0
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
gorm.io/driver/sqlite v1.1.4

51
go.sum
View File

@ -2,9 +2,16 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro=
github.com/99designs/gqlgen v0.13.0 h1:haLTcUp3Vwp80xMVEg5KRNwzfUrgFdRmtBY8fuB8scA=
github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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/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/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/aws/aws-sdk-go v1.20.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -22,6 +29,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dsoprea/go-exif v0.0.0-20201216222538-db167117f483 h1:zJb7OUzMMSul61UUhYXWNOXc9nO1lexj3jsAgoDtCqg=
@ -41,6 +49,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.1.2 h1:gaPnPcNor5aZSVCJVSGipcpbgMWiAAj9z182ocSGbHU=
github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
@ -52,6 +61,7 @@ github.com/go-macaron/gzip v0.0.0-20200329073552-98214d7a897e/go.mod h1:1if9hBU2
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI=
github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
@ -77,9 +87,14 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -98,6 +113,9 @@ github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eT
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lestrrat-go/backoff/v2 v2.0.3 h1:2ABaTa5ifB1L90aoRMjaPa97p0WzzVe93Vggv8oZftw=
github.com/lestrrat-go/backoff/v2 v2.0.3/go.mod h1:mU93bMXuG27/Y5erI5E9weqavpTX5qiVFZI4uXAX0xk=
github.com/lestrrat-go/httpcc v0.0.0-20210101035852-e7e8fea419e3 h1:e52qvXxpJPV/Kb2ovtuYgcRFjNmf9ntcn8BPIbpRM4k=
@ -109,14 +127,24 @@ github.com/lestrrat-go/jwx v1.0.8/go.mod h1:6XJ5sxHF5U116AxYxeHfTnfsZRMgmeKY214z
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10=
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
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/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/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -131,11 +159,15 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
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.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns=
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/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
@ -148,6 +180,7 @@ github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:s
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -159,8 +192,13 @@ github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e h1:GSGeB9EAKY2spCABz6x
github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
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/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns=
github.com/vektah/gqlparser/v2 v2.1.0/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
github.com/vimeo/go-util v1.2.0/go.mod h1:s13SMDTSO7AjH1nbgp707mfN5JFIWUFDU5MDDuRRtKs=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -186,6 +224,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
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/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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -215,11 +255,13 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@ -230,17 +272,22 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290 h1:NXNmtp0ToD36cui5IqWy95LC4Y6vT/4y3RnPxlQPinU=
golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
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-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
@ -261,6 +308,7 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
gopkg.in/Acconut/lockfile.v1 v1.1.0/go.mod h1:6UCz3wJ8tSFUsPR6uP/j8uegEtDuEEqFxlpi0JI4Umw=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/h2non/gock.v1 v1.0.14/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@ -270,6 +318,7 @@ gopkg.in/macaron.v1 v1.4.0/go.mod h1:uMZCFccv9yr5TipIalVOyAyZQuOH3OkmXvgcWwhJuP4
gopkg.in/yaml.v2 v2.2.1/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.3/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.7/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=
@ -284,3 +333,5 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=

56
gqlgen.yml Normal file
View File

@ -0,0 +1,56 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- graph/*.graphqls
# Where should the generated server code go?
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:
- "reichard.io/imagini/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

9341
graph/generated/generated.go Normal file

File diff suppressed because it is too large Load Diff

397
graph/model/models_gen.go Normal file
View File

@ -0,0 +1,397 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package model
import (
"fmt"
"io"
"strconv"
"time"
"github.com/99designs/gqlgen/graphql"
)
type Album struct {
ID *string `json:"id" gorm:"primarykey;not null"`
CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" `
Name string `json:"name" gorm:"unique;not null"`
}
type AlbumFilter struct {
ID *IDFilter `json:"id" `
CreatedAt *TimeFilter `json:"createdAt" `
UpdatedAt *TimeFilter `json:"updatedAt" `
Name *StringFilter `json:"name" `
And *MediaItemFilter `json:"and" `
Or *MediaItemFilter `json:"or" `
}
type AlbumResponse struct {
Data []*Album `json:"data" `
PageInfo *PageInfo `json:"pageInfo" `
}
type AuthTypeFilter struct {
EqualTo *AuthType `json:"equalTo" `
NotEqualTo *AuthType `json:"notEqualTo" `
In []AuthType `json:"in" `
NotIn []AuthType `json:"notIn" `
}
type BooleanFilter struct {
EqualTo *bool `json:"equalTo" `
NotEqualTo *bool `json:"notEqualTo" `
}
type Device struct {
ID *string `json:"id" gorm:"primarykey;not null"`
CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" `
Name string `json:"name" gorm:"not null"`
Type DeviceType `json:"type" gorm:"default:Unknown;not null"`
User *User `json:"user" gorm:"ForeignKey:ID;not null"`
RefreshKey *string `json:"refreshKey" `
}
type DeviceFilter struct {
ID *IDFilter `json:"id" `
CreatedAt *TimeFilter `json:"createdAt" `
UpdatedAt *TimeFilter `json:"updatedAt" `
Name *StringFilter `json:"name" `
Type *DeviceTypeFilter `json:"type" `
And *MediaItemFilter `json:"and" `
Or *MediaItemFilter `json:"or" `
}
type DeviceResponse struct {
Data []*Device `json:"data" `
PageInfo *PageInfo `json:"pageInfo" `
}
type DeviceTypeFilter struct {
EqualTo *DeviceType `json:"equalTo" `
NotEqualTo *DeviceType `json:"notEqualTo" `
In []DeviceType `json:"in" `
NotIn []DeviceType `json:"notIn" `
}
type FloatFilter struct {
EqualTo *float64 `json:"equalTo" `
NotEqualTo *float64 `json:"notEqualTo" `
LessThan *float64 `json:"lessThan" `
LessThanOrEqualTo *float64 `json:"lessThanOrEqualTo" `
MoreThan *float64 `json:"moreThan" `
MoreThanOrEqualTo *float64 `json:"moreThanOrEqualTo" `
In []float64 `json:"in" `
NotIn []float64 `json:"notIn" `
}
type IDFilter struct {
EqualTo *string `json:"equalTo" `
NotEqualTo *string `json:"notEqualTo" `
In []string `json:"in" `
NotIn []string `json:"notIn" `
}
type IntFilter struct {
EqualTo *int `json:"equalTo" `
NotEqualTo *int `json:"notEqualTo" `
LessThan *int `json:"lessThan" `
LessThanOrEqualTo *int `json:"lessThanOrEqualTo" `
MoreThan *int `json:"moreThan" `
MoreThanOrEqualTo *int `json:"moreThanOrEqualTo" `
In []int `json:"in" `
NotIn []int `json:"notIn" `
}
type MediaItem struct {
ID *string `json:"id" gorm:"primarykey;not null"`
CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" `
ExifDate *time.Time `json:"exifDate" `
Latitude *float64 `json:"latitude" `
Longitude *float64 `json:"longitude" `
IsVideo bool `json:"isVideo" gorm:"default:false;not null"`
FileName string `json:"fileName" gorm:"not null"`
OrigName string `json:"origName" gorm:"not null"`
Tags []*Tag `json:"tags" gorm:"many2many:media_tags"`
Albums []*Album `json:"albums" gorm:"many2many:media_albums"`
User *User `json:"user" gorm:"ForeignKey:ID;not null"`
}
type MediaItemFilter struct {
ID *IDFilter `json:"id" `
CreatedAt *TimeFilter `json:"createdAt" `
UpdatedAt *TimeFilter `json:"updatedAt" `
ExifDate *TimeFilter `json:"exifDate" `
Latitude *FloatFilter `json:"latitude" `
Longitude *FloatFilter `json:"longitude" `
IsVideo *BooleanFilter `json:"isVideo" `
OrigName *StringFilter `json:"origName" `
Tags *TagFilter `json:"tags" `
Albums *AlbumFilter `json:"albums" `
And *MediaItemFilter `json:"and" `
Or *MediaItemFilter `json:"or" `
}
type MediaItemResponse struct {
Data []*MediaItem `json:"data" `
PageInfo *PageInfo `json:"pageInfo" `
}
type NewAlbum struct {
Name string `json:"name" `
}
type NewDevice struct {
Name string `json:"name" `
}
type NewMediaItem struct {
File graphql.Upload `json:"file" `
Tags []string `json:"tags" `
Albums []string `json:"albums" `
}
type NewTag struct {
Name string `json:"name" `
}
type NewUser struct {
Email string `json:"email" `
Username string `json:"username" `
FirstName *string `json:"firstName" `
LastName *string `json:"lastName" `
Role Role `json:"role" `
AuthType AuthType `json:"authType" `
Password *string `json:"password" `
}
type PageInfo struct {
Count int `json:"count" `
Page int `json:"page" `
Total int `json:"total" `
}
type RoleFilter struct {
EqualTo *Role `json:"equalTo" `
NotEqualTo *Role `json:"notEqualTo" `
In []Role `json:"in" `
NotIn []Role `json:"notIn" `
}
type StringFilter struct {
EqualTo *string `json:"equalTo" `
NotEqualTo *string `json:"notEqualTo" `
StartWith *string `json:"startWith" `
NotStartWith *string `json:"notStartWith" `
EndWith *string `json:"endWith" `
NotEndWith *string `json:"notEndWith" `
Contain *string `json:"contain" `
NotContain *string `json:"notContain" `
In []string `json:"in" `
NotIn []string `json:"notIn" `
StartWithStrict *string `json:"startWithStrict" `
NotStartWithStrict *string `json:"notStartWithStrict" `
EndWithStrict *string `json:"endWithStrict" `
NotEndWithStrict *string `json:"notEndWithStrict" `
ContainStrict *string `json:"containStrict" `
NotContainStrict *string `json:"notContainStrict" `
}
type Tag struct {
ID *string `json:"id" gorm:"primarykey;not null"`
CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" `
Name string `json:"name" gorm:"unique;not null"`
}
type TagFilter struct {
ID *IDFilter `json:"id" `
CreatedAt *TimeFilter `json:"createdAt" `
UpdatedAt *TimeFilter `json:"updatedAt" `
Name *StringFilter `json:"name" `
And *MediaItemFilter `json:"and" `
Or *MediaItemFilter `json:"or" `
}
type TagResponse struct {
Data []*Tag `json:"data" `
PageInfo *PageInfo `json:"pageInfo" `
}
type TimeFilter struct {
EqualTo *time.Time `json:"equalTo" `
NotEqualTo *time.Time `json:"notEqualTo" `
LessThan *time.Time `json:"lessThan" `
LessThanOrEqualTo *time.Time `json:"lessThanOrEqualTo" `
MoreThan *time.Time `json:"moreThan" `
MoreThanOrEqualTo *time.Time `json:"moreThanOrEqualTo" `
}
type User struct {
ID *string `json:"id" gorm:"primarykey;not null"`
CreatedAt *time.Time `json:"createdAt" `
UpdatedAt *time.Time `json:"updatedAt" `
Email string `json:"email" gorm:"not null;unique"`
Username string `json:"username" gorm:"not null;unique"`
FirstName *string `json:"firstName" `
LastName *string `json:"lastName" `
Role Role `json:"role" gorm:"default:User;not null"`
AuthType AuthType `json:"authType" gorm:"default:Local;not null"`
Password *string `json:"password" `
}
type UserFilter struct {
ID *IDFilter `json:"id" `
CreatedAt *TimeFilter `json:"createdAt" `
UpdatedAt *TimeFilter `json:"updatedAt" `
Username *StringFilter `json:"username" `
FirstName *StringFilter `json:"firstName" `
LastName *StringFilter `json:"lastName" `
Role *RoleFilter `json:"role" `
AuthType *AuthTypeFilter `json:"authType" `
And *UserFilter `json:"and" `
Or *UserFilter `json:"or" `
}
type UserResponse struct {
Data []*User `json:"data" `
PageInfo *PageInfo `json:"pageInfo" `
}
type AuthType string
const (
AuthTypeLocal AuthType = "Local"
AuthTypeLdap AuthType = "LDAP"
)
var AllAuthType = []AuthType{
AuthTypeLocal,
AuthTypeLdap,
}
func (e AuthType) IsValid() bool {
switch e {
case AuthTypeLocal, AuthTypeLdap:
return true
}
return false
}
func (e AuthType) String() string {
return string(e)
}
func (e *AuthType) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = AuthType(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid AuthType", str)
}
return nil
}
func (e AuthType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type DeviceType string
const (
DeviceTypeIOs DeviceType = "iOS"
DeviceTypeAndroid DeviceType = "Android"
DeviceTypeChrome DeviceType = "Chrome"
DeviceTypeFirefox DeviceType = "Firefox"
DeviceTypeInternetExplorer DeviceType = "InternetExplorer"
DeviceTypeEdge DeviceType = "Edge"
DeviceTypeSafari DeviceType = "Safari"
DeviceTypeUnknown DeviceType = "Unknown"
)
var AllDeviceType = []DeviceType{
DeviceTypeIOs,
DeviceTypeAndroid,
DeviceTypeChrome,
DeviceTypeFirefox,
DeviceTypeInternetExplorer,
DeviceTypeEdge,
DeviceTypeSafari,
DeviceTypeUnknown,
}
func (e DeviceType) IsValid() bool {
switch e {
case DeviceTypeIOs, DeviceTypeAndroid, DeviceTypeChrome, DeviceTypeFirefox, DeviceTypeInternetExplorer, DeviceTypeEdge, DeviceTypeSafari, DeviceTypeUnknown:
return true
}
return false
}
func (e DeviceType) String() string {
return string(e)
}
func (e *DeviceType) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = DeviceType(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid DeviceType", str)
}
return nil
}
func (e DeviceType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type Role string
const (
RoleAdmin Role = "Admin"
RoleUser Role = "User"
)
var AllRole = []Role{
RoleAdmin,
RoleUser,
}
func (e Role) IsValid() bool {
switch e {
case RoleAdmin, RoleUser:
return true
}
return false
}
func (e Role) String() string {
return string(e)
}
func (e *Role) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = Role(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid Role", str)
}
return nil
}
func (e Role) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

13
graph/resolver.go Normal file
View File

@ -0,0 +1,13 @@
package graph
import (
"reichard.io/imagini/internal/db"
)
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct{
DB *db.DBManager
}

356
graph/schema.graphqls Normal file
View File

@ -0,0 +1,356 @@
# https://gqlgen.com/reference/scalars/
scalar Time
scalar Upload
# https://gqlgen.com/reference/directives/
directive @hasRole(role: Role!) on FIELD_DEFINITION
directive @meta(
gorm: String,
) on OBJECT | FIELD_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION | ENUM | INPUT_OBJECT | ARGUMENT_DEFINITION
enum Role {
Admin
User
}
enum DeviceType {
iOS
Android
Chrome
Firefox
InternetExplorer
Edge
Safari
Unknown
}
enum AuthType {
Local
LDAP
}
# ------------------------------------------------------------
# ----------------------- Type Filters -----------------------
# ------------------------------------------------------------
input TimeFilter {
equalTo: Time
notEqualTo: Time
lessThan: Time
lessThanOrEqualTo: Time
moreThan: Time
moreThanOrEqualTo: Time
}
input IntFilter {
equalTo: Int
notEqualTo: Int
lessThan: Int
lessThanOrEqualTo: Int
moreThan: Int
moreThanOrEqualTo: Int
in: [Int!]
notIn: [Int!]
}
input FloatFilter {
equalTo: Float
notEqualTo: Float
lessThan: Float
lessThanOrEqualTo: Float
moreThan: Float
moreThanOrEqualTo: Float
in: [Float!]
notIn: [Float!]
}
input BooleanFilter {
equalTo: Boolean
notEqualTo: Boolean
}
input IDFilter {
equalTo: ID
notEqualTo: ID
in: [ID!]
notIn: [ID!]
}
input StringFilter {
equalTo: String
notEqualTo: String
startWith: String
notStartWith: String
endWith: String
notEndWith: String
contain: String
notContain: String
in: [String!]
notIn: [String!]
startWithStrict: String
notStartWithStrict: String
endWithStrict: String
notEndWithStrict: String
containStrict: String
notContainStrict: String
}
input RoleFilter {
equalTo: Role
notEqualTo: Role
in: [Role!]
notIn: [Role!]
}
input DeviceTypeFilter {
equalTo: DeviceType
notEqualTo: DeviceType
in: [DeviceType!]
notIn: [DeviceType!]
}
input AuthTypeFilter {
equalTo: AuthType
notEqualTo: AuthType
in: [AuthType!]
notIn: [AuthType!]
}
# ------------------------------------------------------------
# -------------------- Object Definitions --------------------
# ------------------------------------------------------------
type Device {
id: ID @meta(gorm: "primarykey;not null")
createdAt: Time
updatedAt: Time
name: String! @meta(gorm: "not null")
type: DeviceType! @meta(gorm: "default:Unknown;not null")
user: User @meta(gorm: "ForeignKey:ID;not null")
refreshKey: String
}
type User {
id: ID @meta(gorm: "primarykey;not null")
createdAt: Time
updatedAt: Time
email: String! @meta(gorm: "not null;unique")
username: String! @meta(gorm: "not null;unique")
firstName: String
lastName: String
role: Role! @meta(gorm: "default:User;not null")
authType: AuthType! @meta(gorm: "default:Local;not null")
password: String
}
type MediaItem {
id: ID @meta(gorm: "primarykey;not null")
createdAt: Time
updatedAt: Time
exifDate: Time
latitude: Float
longitude: Float
isVideo: Boolean! @meta(gorm: "default:false;not null")
fileName: String! @meta(gorm: "not null")
origName: String! @meta(gorm: "not null")
tags: [Tag] @meta(gorm: "many2many:media_tags")
albums: [Album] @meta(gorm: "many2many:media_albums")
user: User @meta(gorm: "ForeignKey:ID;not null")
}
type Tag {
id: ID @meta(gorm: "primarykey;not null")
createdAt: Time
updatedAt: Time
name: String! @meta(gorm: "unique;not null")
}
type Album {
id: ID @meta(gorm: "primarykey;not null")
createdAt: Time
updatedAt: Time
name: String! @meta(gorm: "unique;not null")
}
# ------------------------------------------------------------
# ---------------------- Object Filters ----------------------
# ------------------------------------------------------------
input UserFilter {
id: IDFilter
createdAt: TimeFilter
updatedAt: TimeFilter
username: StringFilter
firstName: StringFilter
lastName: StringFilter
role: RoleFilter
authType: AuthTypeFilter
and: UserFilter
or: UserFilter
}
input MediaItemFilter {
id: IDFilter
createdAt: TimeFilter
updatedAt: TimeFilter
exifDate: TimeFilter
latitude: FloatFilter
longitude: FloatFilter
isVideo: BooleanFilter
origName: StringFilter
tags: TagFilter
albums: AlbumFilter
and: MediaItemFilter
or: MediaItemFilter
}
input DeviceFilter {
id: IDFilter
createdAt: TimeFilter
updatedAt: TimeFilter
name: StringFilter
type: DeviceTypeFilter
and: MediaItemFilter
or: MediaItemFilter
}
input TagFilter {
id: IDFilter
createdAt: TimeFilter
updatedAt: TimeFilter
name: StringFilter
and: MediaItemFilter
or: MediaItemFilter
}
input AlbumFilter {
id: IDFilter
createdAt: TimeFilter
updatedAt: TimeFilter
name: StringFilter
and: MediaItemFilter
or: MediaItemFilter
}
# ------------------------------------------------------------
# -------------------------- Inputs --------------------------
# ------------------------------------------------------------
input NewUser {
email: String!
username: String!
firstName: String
lastName: String
role: Role!
authType: AuthType!
password: String
}
input NewDevice {
name: String!
}
input NewMediaItem {
file: Upload!
tags: [ID!]
albums: [ID!]
}
input NewTag {
name: String!
}
input NewAlbum {
name: String!
}
# ------------------------------------------------------------
# ------------------------ Responses -------------------------
# ------------------------------------------------------------
type PageInfo {
count: Int!
page: Int!
total: Int!
}
type MediaItemResponse {
data: [MediaItem]
pageInfo: PageInfo!
}
type UserResponse {
data: [User]
pageInfo: PageInfo!
}
type DeviceResponse {
data: [Device]
pageInfo: PageInfo!
}
type TagResponse {
data: [Tag]
pageInfo: PageInfo!
}
type AlbumResponse {
data: [Album]
pageInfo: PageInfo!
}
# ------------------------------------------------------------
# --------------------- Query & Mutations --------------------
# ------------------------------------------------------------
type Query {
# Single Item
mediaItem(id: ID!): MediaItem! @hasRole(role: User)
device(id: ID!): Device! @hasRole(role: User)
album(id: ID!): Album! @hasRole(role: User)
tag(id: ID!): Tag! @hasRole(role: User)
user(id: ID!): User! @hasRole(role: Admin)
# All
mediaItems(
filter: MediaItemFilter
count: Int
page: Int
): MediaItemResponse! @hasRole(role: User)
devices(
filter: DeviceFilter
count: Int
page: Int
): DeviceResponse! @hasRole(role: User)
albums(
filter: AlbumFilter
count: Int
page: Int
): AlbumResponse! @hasRole(role: User)
tags(
filter: TagFilter
count: Int
page: Int
): TagResponse! @hasRole(role: User)
users(
filter: UserFilter
count: Int
page: Int
): UserResponse! @hasRole(role: Admin)
}
type Mutation {
createMediaItem(input: NewMediaItem!): MediaItem! @hasRole(role: User)
createDevice(input: NewDevice!): Device! @hasRole(role: User)
createAlbum(input: NewAlbum!): Album! @hasRole(role: User)
createTag(input: NewTag!): Tag! @hasRole(role: User)
createUser(input: NewUser!): User! @hasRole(role: Admin)
}

81
graph/schema.resolvers.go Normal file
View File

@ -0,0 +1,81 @@
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"fmt"
"reichard.io/imagini/graph/generated"
"reichard.io/imagini/graph/model"
)
func (r *mutationResolver) CreateMediaItem(ctx context.Context, input model.NewMediaItem) (*model.MediaItem, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) CreateDevice(ctx context.Context, input model.NewDevice) (*model.Device, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) CreateAlbum(ctx context.Context, input model.NewAlbum) (*model.Album, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) CreateTag(ctx context.Context, input model.NewTag) (*model.Tag, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) MediaItem(ctx context.Context, id string) (*model.MediaItem, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Device(ctx context.Context, id string) (*model.Device, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Album(ctx context.Context, id string) (*model.Album, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) MediaItems(ctx context.Context, filter *model.MediaItemFilter, count *int, page *int) (*model.MediaItemResponse, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Devices(ctx context.Context, filter *model.DeviceFilter, count *int, page *int) (*model.DeviceResponse, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Albums(ctx context.Context, filter *model.AlbumFilter, count *int, page *int) (*model.AlbumResponse, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Tags(ctx context.Context, filter *model.TagFilter, count *int, page *int) (*model.TagResponse, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Users(ctx context.Context, filter *model.UserFilter, count *int, page *int) (*model.UserResponse, error) {
panic(fmt.Errorf("not implemented"))
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

View File

@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) albumsHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@ -11,6 +11,7 @@ import (
"github.com/lestrrat-go/jwx/jwt"
"reichard.io/imagini/internal/models"
graphql "reichard.io/imagini/graph/model"
)
func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) {
@ -81,47 +82,47 @@ func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) {
/**
* This will find or create the requested device based on ID and User.
**/
func (api *API) upsertRequestedDevice(user models.User, r *http.Request) (models.Device, error) {
func (api *API) upsertRequestedDevice(user graphql.User, r *http.Request) (graphql.Device, error) {
requestedDevice := deriveRequestedDevice(r)
requestedDevice.Type = deriveDeviceType(r)
requestedDevice.UserUUID = user.UUID
requestedDevice.User.ID = user.ID
if requestedDevice.UUID == uuid.Nil {
if *requestedDevice.ID == "" {
err := api.DB.CreateDevice(&requestedDevice)
createdDevice, err := api.DB.Device(&requestedDevice)
return createdDevice, err
}
foundDevice, err := api.DB.Device(&models.Device{
Base: models.Base{ UUID: requestedDevice.UUID },
User: user,
foundDevice, err := api.DB.Device(&graphql.Device{
ID: requestedDevice.ID,
User: &user,
})
return foundDevice, err
}
func deriveDeviceType(r *http.Request) string {
func deriveDeviceType(r *http.Request) graphql.DeviceType {
userAgent := strings.ToLower(r.Header.Get("User-Agent"))
if strings.HasPrefix(userAgent, "ios-imagini"){
return "iOS"
return graphql.DeviceTypeIOs
} else if strings.HasPrefix(userAgent, "android-imagini"){
return "Android"
return graphql.DeviceTypeAndroid
} else if strings.HasPrefix(userAgent, "chrome"){
return "Chrome"
return graphql.DeviceTypeChrome
} else if strings.HasPrefix(userAgent, "firefox"){
return "Firefox"
return graphql.DeviceTypeFirefox
} else if strings.HasPrefix(userAgent, "msie"){
return "Internet Explorer"
return graphql.DeviceTypeInternetExplorer
} else if strings.HasPrefix(userAgent, "edge"){
return "Edge"
return graphql.DeviceTypeEdge
} else if strings.HasPrefix(userAgent, "safari"){
return "Safari"
return graphql.DeviceTypeSafari
}
return "Unknown"
return graphql.DeviceTypeUnknown
}
func deriveRequestedDevice(r *http.Request) models.Device {
deviceSkeleton := models.Device{}
func deriveRequestedDevice(r *http.Request) graphql.Device {
deviceSkeleton := graphql.Device{}
authHeader := r.Header.Get("X-Imagini-Authorization")
splitAuthInfo := strings.Split(authHeader, ",")
@ -137,19 +138,20 @@ func deriveRequestedDevice(r *http.Request) models.Device {
// Derive Key
key := strings.ToLower(strings.TrimSpace(splitItem[0]))
if key != "deviceuuid" && key != "devicename" {
if key != "deviceid" && key != "devicename" {
continue
}
// Derive Value
val := trimQuotes(strings.TrimSpace(splitItem[1]))
if key == "deviceuuid" {
if key == "deviceid" {
parsedDeviceUUID, err := uuid.Parse(val)
if err != nil {
log.Warn("[auth] deriveRequestedDevice - Unable to parse requested DeviceUUID: ", val)
continue
}
deviceSkeleton.Base = models.Base{UUID: parsedDeviceUUID}
stringDeviceUUID := parsedDeviceUUID.String()
deviceSkeleton.ID = &stringDeviceUUID
} else if key == "devicename" {
deviceSkeleton.Name = val
}
@ -157,7 +159,7 @@ func deriveRequestedDevice(r *http.Request) models.Device {
// If name not set, set to type
if deviceSkeleton.Name == "" {
deviceSkeleton.Name = deviceSkeleton.Type
deviceSkeleton.Name = deviceSkeleton.Type.String()
}
return deviceSkeleton
@ -196,9 +198,12 @@ func (api *API) refreshAccessToken(w http.ResponseWriter, r *http.Request) (jwt.
return nil, err
}
stringUserUUID := userUUID.String()
stringDeviceUUID := deviceUUID.String()
// Device & User Skeleton
user := models.User{Base: models.Base{UUID: userUUID}}
device := models.Device{Base: models.Base{UUID: deviceUUID}}
user := graphql.User{ID: &stringUserUUID}
device := graphql.Device{ID: &stringDeviceUUID}
// Update token
accessTokenString, err := api.Auth.CreateJWTAccessToken(user, device)

View File

@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) devicesHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) infoHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@ -1,327 +0,0 @@
package api
import (
"io"
"os"
"fmt"
"path"
"time"
"regexp"
"strings"
"errors"
"net/url"
"net/http"
"encoding/json"
"github.com/google/uuid"
"github.com/dsoprea/go-exif/v3"
log "github.com/sirupsen/logrus"
"github.com/gabriel-vasile/mimetype"
"github.com/dsoprea/go-exif/v3/common"
"reichard.io/imagini/internal/models"
)
// GET
// - /api/v1/MediaItems/<GUID>
// - JSON Struct
// - /api/v1/MediaItems/<GUID>/content
// - The raw file
func (api *API) mediaItemsHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
// CREATE
api.mediaItemPOSTHandler(w, r)
} else if r.Method == http.MethodPut {
// UPDATE / REPLACE
} else if r.Method == http.MethodPatch {
// UPDATE / MODIFY
} else if r.Method == http.MethodDelete {
// DELETE
} else if r.Method == http.MethodGet {
// GET
api.mediaItemGETHandler(w, r)
} else {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
return
}
}
// Paging:
// - Regular Pagination:
// - /api/v1/MediaItems?page[limit]=50&page=2
// - Meta Count Only
// - /api/v1/MediaItems?page[limit]=0
// Sorting:
// - Ascending Sort:
// - /api/v1/MediaItems?sort=created_at
// - Descending Sort:
// - /api/v1/MediaItems?sort=-created_at
// Filters:
// - Greater Than / Less Than (created_at, updated_at, exif_date)
// - /api/v1/MediaItems?filter[created_at]>=2020-01-01&filter[created_at]<=2021-01-01
// - Long / Lat Range (latitude, longitude)
// - /api/v1/MediaItems?filter[latitude]>=71.1827&filter[latitude]<=72.0000&filter[longitude]>=100.000&filter[longitude]<=101.0000
// - Image / Video (media_type)
// - /api/v1/MediaItems?filter[media_type]=Image
// - Tags (tags)
// - /api/v1/MediaItems?filter[tags]=id1,id2,id3
// - Albums (albums)
// - /api/v1/MediaItems?filter[albums]=id1
func (api *API) mediaItemGETHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
// Handle error
}
testObj := models.MediaItem{}
json.NewDecoder().Decode(&testObj)
fmt.Printf("Result: %+v\n", testObj)
// allParams, err := json.Marshal(r.Form)
// if err != nil {
// // Handle error
// }
// filter := &models.MediaItem{}
// if err = json.Unmarshal(allParams, filter); err != nil {
// // Handle error
// fmt.Printf("Fuck: %s\n", err)
// }
// fmt.Printf("Result: %+v\n", filter)
// err = normalizeForm(r.Form, models.MediaItem{})
// if err != nil {
// fmt.Printf("Error: %s\n", err)
// }
// var testItems []models.MediaItem
// api.DB.QueryBuilder(&testItems, allParams)
// fmt.Printf("\n\nItems: %+v", testItems)
// Pull out UUIDs
reqInfo := r.Context().Value("uuids").(map[string]string)
uid := reqInfo["uid"]
userUUID, _ := uuid.Parse(uid)
// TODO: Can apply multiple filters based on query parameters
mediaItemFilter := &models.MediaItem{UserUUID: userUUID}
mediaItemFilter.UserUUID = userUUID
mediaItems, count, _ := api.DB.MediaItems(mediaItemFilter)
response := &models.APIResponse{
Data: &mediaItems,
Meta: &models.APIMeta{Count: count},
}
responseJSON(w, &response, http.StatusOK)
}
func (api *API) mediaItemPOSTHandler(w http.ResponseWriter, r *http.Request) {
// 64MB limit (TODO: Change this - video)
r.ParseMultipartForm(64 << 20)
// Open form file
formFile, multipartFileHeader, err := r.FormFile("file")
if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
defer formFile.Close()
// File header placeholder
fileHeader := make([]byte, 64)
// Copy headers into the buffer
if _, err := formFile.Read(fileHeader); err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
// Reset position
if _, err := formFile.Seek(0, 0); err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
// Determine media type
fileMime := mimetype.Detect(fileHeader)
contentType := fileMime.String()
var mediaType string
if strings.HasPrefix(contentType, "image/") {
mediaType = "Image"
} else if strings.HasPrefix(contentType, "video/") {
mediaType = "Video"
} else {
errorJSON(w, "Invalid filetype.", http.StatusUnsupportedMediaType)
return
}
// Pull out UUIDs
reqInfo := r.Context().Value("uuids").(map[string]string)
uid := reqInfo["uid"]
// Derive Folder & File Path
mediaItemUUID := uuid.New()
fileName := mediaItemUUID.String() + fileMime.Extension()
folderPath := path.Join("/" + api.Config.DataPath + "/media/" + uid)
os.MkdirAll(folderPath, 0700)
filePath := path.Join(folderPath + "/" + fileName)
// Create File
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
log.Warn("[api] createMediaItem - Unable to open file: ", filePath)
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
defer f.Close()
// Copy data to file
_, err = io.Copy(f, formFile)
if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
// Create MediaItem From EXIF Data
mediaItem, err := mediaItemFromEXIFData(filePath)
if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
// Add Additional MediaItem Fields
mediaItem.Base.UUID = mediaItemUUID
mediaItem.UserUUID, err = uuid.Parse(uid)
mediaItem.MediaType = mediaType
mediaItem.FileName = fileName
mediaItem.OrigName = multipartFileHeader.Filename
// Create MediaItem in DB
err = api.DB.CreateMediaItem(mediaItem)
if err != nil {
errorJSON(w, "Upload failed.", http.StatusInternalServerError)
return
}
successJSON(w, "Upload succeeded.", http.StatusCreated)
}
func mediaItemFromEXIFData(filePath string) (*models.MediaItem, error) {
rawExif, err := exif.SearchFileAndExtractExif(filePath)
entries, _, err := exif.GetFlatExifData(rawExif, nil)
decLong := float32(1)
decLat := float32(1)
mediaItem := &models.MediaItem{}
for _, v := range entries {
if v.TagName == "DateTimeOriginal" {
formattedTime, _ := time.Parse("2006:01:02 15:04:05", v.Formatted)
mediaItem.EXIFDate = formattedTime
} else if v.TagName == "GPSLatitude" {
latStruct := v.Value.([]exifcommon.Rational)
decLat *= deriveDecimalCoordinate(
latStruct[0].Numerator / latStruct[0].Denominator,
latStruct[1].Numerator / latStruct[1].Denominator,
float32(latStruct[2].Numerator) / float32(latStruct[2].Denominator),
)
} else if v.TagName == "GPSLongitude" {
longStruct := v.Value.([]exifcommon.Rational)
decLong *= deriveDecimalCoordinate(
longStruct[0].Numerator / longStruct[0].Denominator,
longStruct[1].Numerator / longStruct[1].Denominator,
float32(longStruct[2].Numerator) / float32(longStruct[2].Denominator),
)
} else if v.TagName == "GPSLatitudeRef" && v.Formatted == "S" {
decLat *= -1
} else if v.TagName == "GPSLongitudeRef" && v.Formatted == "W" {
decLong *= -1
}
}
mediaItem.Latitude = &decLat
mediaItem.Longitude = &decLong
return mediaItem, err
}
func deriveDecimalCoordinate(degrees, minutes uint32, seconds float32) float32 {
return float32(degrees) + (float32(minutes) / 60) + (seconds / 3600)
}
// {
// filters: [
// { field: "", operator: ""},
// { field: "", operator: ""},
// { field: "", operator: ""},
// ],
// sort: ""
// page: {}
// }
func normalizeForm(form url.Values, typeObj interface{}) error {
allowedFields := models.JSONFields(typeObj)
for key, val := range form {
key = strings.ToLower(key)
re := regexp.MustCompile(`^(filter|page)\[(\w*)]($|>|<)$`)
matches := re.FindStringSubmatch(key)
if len(matches) == 4 {
cmd := strings.ToLower(matches[1])
field := strings.ToLower(matches[2])
operator := strings.ToLower(matches[3])
if cmd == "page" && field == "limit" {
fmt.Printf("cmd: %s field: %s op: %s\n", cmd, field, operator)
continue
}
// Validate field
_, ok := allowedFields[field]
if !ok {
return errors.New("Invalid field.")
}
// Val assertions
tempObj := make(map[string]string)
tempObj[field] = val[0]
mi, err := json.Marshal(tempObj)
if err != nil {
// Handle error
fmt.Printf("1 Type Assertion Failed For Field: [%s] with value: [%s]\n", field, val)
}
fmt.Printf("String JSON: %s", string(mi))
refObj := &models.MediaItem{}
if err = json.Unmarshal(mi, refObj); err != nil {
// Handle error
fmt.Printf("2 Type Assertion Failed For Field: [%s] with value: [%s]\n", field, val[0])
fmt.Println(err)
}
fmt.Printf("Result: %+v\n", refObj)
fmt.Printf("cmd: %s field: %s op: %s\n", cmd, field, operator)
} else if key == "sort" {
field := strings.ToLower(val[0])
// Validate field
_, ok := allowedFields[field]
if !ok {
return errors.New("Invalid field.")
}
// TODO: Validate val
fmt.Printf("cmd: %s\n", key)
} else {
return errors.New("Invalid parameter(s)")
}
}
return nil
}

View File

@ -5,53 +5,27 @@ import (
"net/http"
"reichard.io/imagini/internal/models"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"reichard.io/imagini/graph"
"reichard.io/imagini/graph/generated"
)
func (api *API) registerRoutes() {
srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
// TODO: Provide Authentication for srv
api.Router.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
api.Router.Handle("/query", srv)
api.Router.HandleFunc("/media/", multipleMiddleware(
api.mediaHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/MediaItems", multipleMiddleware(
api.mediaItemsHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Devices", multipleMiddleware(
api.devicesHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Upload", multipleMiddleware(
api.uploadHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Albums", multipleMiddleware(
api.albumsHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Users", multipleMiddleware(
api.usersHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Tags", multipleMiddleware(
api.tagsHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Info", multipleMiddleware(
api.infoHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Me", multipleMiddleware(
api.meHandler,
api.authMiddleware,
))
api.Router.HandleFunc("/api/v1/Logout", api.logoutHandler)
api.Router.HandleFunc("/api/v1/Login", api.loginHandler)
api.Router.HandleFunc("/logout", api.logoutHandler)
api.Router.HandleFunc("/login", api.loginHandler)
}
// https://stackoverflow.com/a/59764037
func errorJSON(w http.ResponseWriter, err string, code int) {
errStruct := &models.APIResponse{Error: &models.APIError{Message: err, Code: int64(code)}}
responseJSON(w, errStruct, code)

View File

@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) tagsHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@ -1,9 +0,0 @@
package api
import (
"net/http"
)
func (api *API) uploadHandler(w http.ResponseWriter, r *http.Request) {
}

View File

@ -1,30 +0,0 @@
package api
import (
"net/http"
// log "github.com/sirupsen/logrus"
)
func (api *API) usersHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
// CREATE
} else if r.Method == http.MethodPut {
// UPDATE / REPLACE
} else if r.Method == http.MethodPatch {
// UPDATE / MODIFY
} else if r.Method == http.MethodDelete {
// DELETE
} else if r.Method == http.MethodGet {
// GET
} else {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
return
}
}
func (api *API) meHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed)
return
}
}

View File

@ -14,6 +14,8 @@ import (
"reichard.io/imagini/internal/db"
"reichard.io/imagini/internal/config"
graphql "reichard.io/imagini/graph/model"
"reichard.io/imagini/internal/models"
"reichard.io/imagini/internal/session"
)
@ -33,11 +35,16 @@ func NewMgr(db *db.DBManager, c *config.Config) *AuthManager {
}
}
func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, models.User) {
// By Username
foundUser, err := auth.DB.User(&models.User{Username: creds.User})
func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, graphql.User) {
// Search Objects
userByName := &graphql.User{}
userByName.Username = creds.User
foundUser, err := auth.DB.User(userByName)
if errors.Is(err, gorm.ErrRecordNotFound) {
foundUser, err = auth.DB.User(&models.User{Email: creds.User})
userByEmail := &graphql.User{}
userByEmail.Email = creds.User
foundUser, err = auth.DB.User(userByEmail)
}
// Error Checking
@ -62,7 +69,7 @@ func (auth *AuthManager) AuthenticateUser(creds models.APICredentials) (bool, mo
}
}
func (auth *AuthManager) getRole(user models.User) string {
func (auth *AuthManager) getRole(user graphql.User) string {
// TODO: Lookup role of user
return "User"
}
@ -80,7 +87,8 @@ func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token,
if err != nil {
return nil, errors.New("did does not parse")
}
device, err := auth.DB.Device(&models.Device{Base: models.Base{UUID: deviceID}})
stringDeviceID := deviceID.String()
device, err := auth.DB.Device(&graphql.Device{ID: &stringDeviceID})
if err != nil {
return nil, err
}
@ -88,7 +96,7 @@ func (auth *AuthManager) ValidateJWTRefreshToken(refreshJWT string) (jwt.Token,
// Verify & Validate Token
verifiedToken, err := jwt.ParseBytes(byteRefreshJWT,
jwt.WithValidate(true),
jwt.WithVerify(jwa.HS256, []byte(device.RefreshKey)),
jwt.WithVerify(jwa.HS256, []byte(*device.RefreshKey)),
)
if err != nil {
fmt.Println("failed to parse payload: ", err)
@ -111,17 +119,17 @@ func (auth *AuthManager) ValidateJWTAccessToken(accessJWT string) (jwt.Token, er
return verifiedToken, nil
}
func (auth *AuthManager) CreateJWTRefreshToken(user models.User, device models.Device) (string, error) {
func (auth *AuthManager) CreateJWTRefreshToken(user graphql.User, device graphql.Device) (string, error) {
// Acquire Refresh Key
byteKey := []byte(device.RefreshKey)
byteKey := []byte(*device.RefreshKey)
// Create New Token
tm := time.Now()
t := jwt.New()
t.Set(`did`, device.UUID.String()) // Device ID
t.Set(jwt.SubjectKey, user.UUID.String()) // User ID
t.Set(jwt.AudienceKey, `imagini`) // App ID
t.Set(jwt.IssuedAtKey, tm) // Issued At
t.Set(`did`, device.ID) // Device ID
t.Set(jwt.SubjectKey, user.ID) // User ID
t.Set(jwt.AudienceKey, `imagini`) // App ID
t.Set(jwt.IssuedAtKey, tm) // Issued At
// iOS & Android = Never Expiring Refresh Token
if device.Type != "iOS" && device.Type != "Android" {
@ -146,16 +154,16 @@ func (auth *AuthManager) CreateJWTRefreshToken(user models.User, device models.D
return string(signed), nil
}
func (auth *AuthManager) CreateJWTAccessToken(user models.User, device models.Device) (string, error) {
func (auth *AuthManager) CreateJWTAccessToken(user graphql.User, device graphql.Device) (string, error) {
// Create New Token
tm := time.Now()
t := jwt.New()
t.Set(`did`, device.UUID.String()) // Device ID
t.Set(`role`, auth.getRole(user)) // User Role (Admin / User)
t.Set(jwt.SubjectKey, user.UUID.String()) // User ID
t.Set(jwt.AudienceKey, `imagini`) // App ID
t.Set(jwt.IssuedAtKey, tm) // Issued At
t.Set(jwt.ExpirationKey, tm.Add(time.Hour * 2)) // 2 Hour Access Key
t.Set(`did`, device.ID) // Device ID
t.Set(`role`, auth.getRole(user)) // User Role (Admin / User)
t.Set(jwt.SubjectKey, user.ID) // User ID
t.Set(jwt.AudienceKey, `imagini`) // App ID
t.Set(jwt.IssuedAtKey, tm) // Issued At
t.Set(jwt.ExpirationKey, tm.Add(time.Hour * 2)) // 2 Hour Access Key
// Validate Token Creation
_, err := json.MarshalIndent(t, "", " ")

View File

@ -1,9 +1,9 @@
package auth
import (
"reichard.io/imagini/internal/models"
"reichard.io/imagini/graph/model"
)
func authenticateLDAPUser(user models.User, pw string) bool {
func authenticateLDAPUser(user model.User, pw string) bool {
return false
}

View File

@ -3,12 +3,12 @@ package auth
import (
"golang.org/x/crypto/bcrypt"
log "github.com/sirupsen/logrus"
"reichard.io/imagini/internal/models"
"reichard.io/imagini/graph/model"
)
func authenticateLocalUser(user models.User, pw string) bool {
func authenticateLocalUser(user model.User, pw string) bool {
bPassword :=[]byte(pw)
err := bcrypt.CompareHashAndPassword([]byte(user.Password), bPassword)
err := bcrypt.CompareHashAndPassword([]byte(*user.Password), bPassword)
if err == nil {
log.Info("[auth] Authentication successfull: ", user.Username)
return true

View File

@ -11,7 +11,7 @@ import (
log "github.com/sirupsen/logrus"
"reichard.io/imagini/internal/config"
"reichard.io/imagini/internal/models"
"reichard.io/imagini/graph/model"
)
type DBManager struct {
@ -35,16 +35,15 @@ func NewMgr(c *config.Config) *DBManager {
}
// Initialize database
dbm.db.AutoMigrate(&models.ServerSetting{})
dbm.db.AutoMigrate(&models.Device{})
dbm.db.AutoMigrate(&models.User{})
dbm.db.AutoMigrate(&models.MediaItem{})
dbm.db.AutoMigrate(&models.Tag{})
dbm.db.AutoMigrate(&models.Album{})
dbm.db.AutoMigrate(&model.Device{})
dbm.db.AutoMigrate(&model.User{})
dbm.db.AutoMigrate(&model.MediaItem{})
dbm.db.AutoMigrate(&model.Tag{})
dbm.db.AutoMigrate(&model.Album{})
// Determine whether to bootstrap
var count int64
dbm.db.Model(&models.User{}).Count(&count)
dbm.db.Model(&model.User{}).Count(&count)
if count == 0 {
dbm.bootstrapDatabase()
}
@ -54,17 +53,23 @@ func NewMgr(c *config.Config) *DBManager {
func (dbm *DBManager) bootstrapDatabase() {
log.Info("[query] Bootstrapping database.")
err := dbm.CreateUser(&models.User{
password := "admin"
user := &model.User{
Username: "admin",
Password: "admin",
AuthType: "Local",
})
Password: &password,
}
err := dbm.CreateUser(user)
if err != nil {
log.Fatal("[query] Unable to bootstrap database.")
}
}
// func (dmb *DBManager) {}
func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, error) {
// TODO:
// - Where Filters
@ -72,7 +77,7 @@ func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, erro
// - Paging Filters
objType := fmt.Sprintf("%T", dest)
if objType == "*[]models.MediaItem" {
if objType == "*[]model.MediaItem" {
// TODO: Validate MediaItem Type
} else {
// Return Error

View File

@ -4,27 +4,28 @@ import (
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"reichard.io/imagini/internal/models"
"reichard.io/imagini/graph/model"
)
func (dbm *DBManager) CreateDevice (device *models.Device) error {
func (dbm *DBManager) CreateDevice (device *model.Device) error {
log.Info("[db] Creating device: ", device.Name)
device.RefreshKey = uuid.New().String()
refreshKey := uuid.New().String()
device.RefreshKey = &refreshKey
err := dbm.db.Create(&device).Error
return err
}
func (dbm *DBManager) Device (device *models.Device) (models.Device, error) {
var foundDevice models.Device
func (dbm *DBManager) Device (device *model.Device) (model.Device, error) {
var foundDevice model.Device
var count int64
err := dbm.db.Where(&device).First(&foundDevice).Count(&count).Error
return foundDevice, err
}
func (dbm *DBManager) DeleteDevice (user *models.Device) error {
func (dbm *DBManager) DeleteDevice (user *model.Device) error {
return nil
}
func (dbm *DBManager) UpdateRefreshToken (device *models.Device, refreshToken string) error {
func (dbm *DBManager) UpdateRefreshToken (device *model.Device, refreshToken string) error {
return nil
}

View File

@ -3,17 +3,17 @@ package db
import (
log "github.com/sirupsen/logrus"
"reichard.io/imagini/internal/models"
"reichard.io/imagini/graph/model"
)
func (dbm *DBManager) CreateMediaItem (mediaItem *models.MediaItem) error {
func (dbm *DBManager) CreateMediaItem (mediaItem *model.MediaItem) error {
log.Info("[db] Creating media item: ", mediaItem.FileName)
err := dbm.db.Create(&mediaItem).Error
return err
}
func (dbm *DBManager) MediaItems(mediaItemFilter *models.MediaItem) ([]models.MediaItem, int64, error) {
var mediaItems []models.MediaItem
func (dbm *DBManager) MediaItems(mediaItemFilter *model.MediaItem) ([]model.MediaItem, int64, error) {
var mediaItems []model.MediaItem
var count int64
err := dbm.db.Where(&mediaItemFilter).Find(&mediaItems).Count(&count).Error;

View File

@ -4,32 +4,33 @@ import (
"golang.org/x/crypto/bcrypt"
log "github.com/sirupsen/logrus"
"reichard.io/imagini/internal/models"
"reichard.io/imagini/graph/model"
)
func (dbm *DBManager) CreateUser(user *models.User) error {
func (dbm *DBManager) CreateUser(user *model.User) error {
log.Info("[db] Creating user: ", user.Username)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*user.Password), bcrypt.DefaultCost)
if err != nil {
log.Error(err)
return err
}
user.Password = string(hashedPassword)
stringHashedPassword := string(hashedPassword)
user.Password = &stringHashedPassword
err = dbm.db.Create(&user).Error
return err
}
func (dbm *DBManager) User (user *models.User) (models.User, error) {
var foundUser models.User
func (dbm *DBManager) User (user *model.User) (model.User, error) {
var foundUser model.User
var count int64
err := dbm.db.Where(&user).First(&foundUser).Count(&count).Error
return foundUser, err
}
func (dbm *DBManager) DeleteUser (user models.User) error {
func (dbm *DBManager) DeleteUser (user model.User) error {
return nil
}
func (dbm *DBManager) UpdatePassword (user models.User, pw string) {
func (dbm *DBManager) UpdatePassword (user model.User, pw string) {
}

View File

@ -1,96 +0,0 @@
package models
import (
"time"
"strings"
"reflect"
"gorm.io/gorm"
"github.com/google/uuid"
)
type Base struct {
UUID uuid.UUID `json:"uuid" gorm:"type:uuid;primarykey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
func (base *Base) BeforeCreate(tx *gorm.DB) (err error) {
base.UUID = uuid.New()
return
}
type ServerSetting struct {
Base
Name string `json:"name" gorm:"not null"`
Description string `json:"description" gorm:"not null"`
Value string `json:"value" gorm:"not null"`
}
type Device struct {
Base
UserUUID uuid.UUID `json:"-" gorm:"not null"`
User User `json:"user" gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User
Name string `json:"name" gorm:"not null"` // Name of Device
Type string `json:"type" gorm:"not null"` // Android, iOS, Chrome, FireFox, Edge
RefreshKey string `json:"-"` // Device Specific Refresh Key
}
type User struct {
Base
Email string `json:"email" gorm:"unique"` // Email
Username string `json:"username" gorm:"unique"` // Username
FirstName string `json:"first_name"` // First Name
LastName string `json:"last_name"` // Last Name
Role string `json:"role"` // Role
AuthType string `json:"auth_type" gorm:"default:Local;not null"` // Auth Type (E.g. Local, LDAP)
Password string `json:"-"` // Hased & Salted Password
}
type MediaItem struct {
Base
UserUUID uuid.UUID `json:"-" gorm:"not null"`
User User `json:"-" gorm:"ForeignKey:UUID;References:UserUUID;not null"` // User
EXIFDate time.Time `json:"exif_date"` // EXIF Date
Latitude *float32 `json:"latitude" gorm:"type:decimal(10,2)"` // Decimal Latitude
Longitude *float32 `json:"longitude" gorm:"type:decimal(10,2)"` // Decimal Longitude
MediaType string `json:"media_type" gorm:"default:Image;not null"` // Image, Video
OrigName string `json:"orig_name" gorm:"not null"` // Original Name
FileName string `json:"file_name" gorm:"not null"` // File Name
Tags []Tag `json:"tags" gorm:"many2many:media_tags;"` // Associated Tag UUIDs
Albums []Album `json:"albums" gorm:"many2many:media_albums;"` // Associated Album UUIDs
}
type Tag struct {
Base
Name string `json:"name" gorm:"not null"` // Tag Name
}
type Album struct {
Base
Name string `json:"name" gorm:"not null"` // Album Name
}
func JSONFields(model interface{}) map[string]struct{} {
jsonFields := make(map[string]struct{})
val := reflect.ValueOf(model)
t := val.Type()
for i := 0; i < t.NumField(); i++ {
jsonField := strings.TrimSpace(t.Field(i).Tag.Get("json"))
if jsonField == "" {
continue
}
jsonSplit := strings.Split(jsonField, ",")
fieldVal := strings.TrimSpace(jsonSplit[0])
if fieldVal == "" || fieldVal == "-" {
continue
}
jsonFields[fieldVal] = struct{}{}
}
return jsonFields
}

240
plugin/models.go Normal file
View File

@ -0,0 +1,240 @@
package plugin
import (
"fmt"
"go/types"
"sort"
"github.com/99designs/gqlgen/codegen/config"
"github.com/99designs/gqlgen/codegen/templates"
"github.com/99designs/gqlgen/plugin"
"github.com/vektah/gqlparser/v2/ast"
)
type BuildMutateHook = func(b *ModelBuild) *ModelBuild
func defaultBuildMutateHook(b *ModelBuild) *ModelBuild {
return b
}
type ModelBuild struct {
PackageName string
Interfaces []*Interface
Models []*Object
Enums []*Enum
Scalars []string
}
type Interface struct {
Description string
Name string
}
type Object struct {
Description string
Name string
Fields []*Field
Implements []string
}
type Field struct {
Description string
Name string
Type types.Type
Tag string
Gorm string
}
type Enum struct {
Description string
Name string
Values []*EnumValue
}
type EnumValue struct {
Description string
Name string
}
func New() plugin.Plugin {
return &Plugin{
MutateHook: defaultBuildMutateHook,
}
}
type Plugin struct {
MutateHook BuildMutateHook
}
var _ plugin.ConfigMutator = &Plugin{}
func (m *Plugin) Name() string {
return "imaginimodel"
}
func (m *Plugin) MutateConfig(cfg *config.Config) error {
binder := cfg.NewBinder()
b := &ModelBuild{
PackageName: cfg.Model.Package,
}
for _, schemaType := range cfg.Schema.Types {
if schemaType.BuiltIn {
continue
}
switch schemaType.Kind {
case ast.Interface, ast.Union:
it := &Interface{
Description: schemaType.Description,
Name: schemaType.Name,
}
b.Interfaces = append(b.Interfaces, it)
case ast.Object, ast.InputObject:
if schemaType == cfg.Schema.Query || schemaType == cfg.Schema.Mutation || schemaType == cfg.Schema.Subscription {
continue
}
it := &Object{
Description: schemaType.Description,
Name: schemaType.Name,
}
for _, implementor := range cfg.Schema.GetImplements(schemaType) {
it.Implements = append(it.Implements, implementor.Name)
}
for _, field := range schemaType.Fields {
var typ types.Type
fieldDef := cfg.Schema.Types[field.Type.Name()]
if cfg.Models.UserDefined(field.Type.Name()) {
var err error
typ, err = binder.FindTypeFromName(cfg.Models[field.Type.Name()].Model[0])
if err != nil {
return err
}
} else {
switch fieldDef.Kind {
case ast.Scalar:
// no user defined model, referencing a default scalar
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), "string", nil),
nil,
nil,
)
case ast.Interface, ast.Union:
// no user defined model, referencing a generated interface type
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
types.NewInterfaceType([]*types.Func{}, []types.Type{}),
nil,
)
case ast.Enum:
// no user defined model, must reference a generated enum
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
nil,
nil,
)
case ast.Object, ast.InputObject:
// no user defined model, must reference a generated struct
typ = types.NewNamed(
types.NewTypeName(0, cfg.Model.Pkg(), templates.ToGo(field.Type.Name()), nil),
types.NewStruct(nil, nil),
nil,
)
default:
panic(fmt.Errorf("unknown ast type %s", fieldDef.Kind))
}
}
name := field.Name
if nameOveride := cfg.Models[schemaType.Name].Fields[field.Name].FieldName; nameOveride != "" {
name = nameOveride
}
typ = binder.CopyModifiersFromAst(field.Type, typ)
if isStruct(typ) && (fieldDef.Kind == ast.Object || fieldDef.Kind == ast.InputObject) {
typ = types.NewPointer(typ)
}
gormType := ""
directive := field.Directives.ForName("meta")
if directive != nil {
arg := directive.Arguments.ForName("gorm")
if arg != nil {
gormType = fmt.Sprintf("gorm:\"%s\"", arg.Value.Raw)
}
}
it.Fields = append(it.Fields, &Field{
Name: name,
Type: typ,
Description: field.Description,
Tag: `json:"` + field.Name + `"`,
Gorm: gormType,
})
}
b.Models = append(b.Models, it)
case ast.Enum:
it := &Enum{
Name: schemaType.Name,
Description: schemaType.Description,
}
for _, v := range schemaType.EnumValues {
it.Values = append(it.Values, &EnumValue{
Name: v.Name,
Description: v.Description,
})
}
b.Enums = append(b.Enums, it)
case ast.Scalar:
b.Scalars = append(b.Scalars, schemaType.Name)
}
}
sort.Slice(b.Enums, func(i, j int) bool { return b.Enums[i].Name < b.Enums[j].Name })
sort.Slice(b.Models, func(i, j int) bool { return b.Models[i].Name < b.Models[j].Name })
sort.Slice(b.Interfaces, func(i, j int) bool { return b.Interfaces[i].Name < b.Interfaces[j].Name })
for _, it := range b.Enums {
cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGo(it.Name))
}
for _, it := range b.Models {
cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGo(it.Name))
}
for _, it := range b.Interfaces {
cfg.Models.Add(it.Name, cfg.Model.ImportPath()+"."+templates.ToGo(it.Name))
}
for _, it := range b.Scalars {
cfg.Models.Add(it, "github.com/99designs/gqlgen/graphql.String")
}
if len(b.Models) == 0 && len(b.Enums) == 0 && len(b.Interfaces) == 0 && len(b.Scalars) == 0 {
return nil
}
if m.MutateHook != nil {
b = m.MutateHook(b)
}
return templates.Render(templates.Options{
PackageName: cfg.Model.Package,
Filename: cfg.Model.Filename,
Data: b,
GeneratedHeader: true,
Packages: cfg.Packages,
})
}
func isStruct(t types.Type) bool {
_, is := t.Underlying().(*types.Struct)
return is
}

85
plugin/models.gotpl Normal file
View File

@ -0,0 +1,85 @@
{{ reserveImport "context" }}
{{ reserveImport "fmt" }}
{{ reserveImport "io" }}
{{ reserveImport "strconv" }}
{{ reserveImport "time" }}
{{ reserveImport "sync" }}
{{ reserveImport "errors" }}
{{ reserveImport "bytes" }}
{{ reserveImport "github.com/vektah/gqlparser/v2" }}
{{ reserveImport "github.com/vektah/gqlparser/v2/ast" }}
{{ reserveImport "github.com/99designs/gqlgen/graphql" }}
{{ reserveImport "github.com/99designs/gqlgen/graphql/introspection" }}
{{- range $model := .Interfaces }}
{{ with .Description }} {{.|prefixLines "// "}} {{ end }}
type {{.Name|go }} interface {
Is{{.Name|go }}()
}
{{- end }}
{{ range $model := .Models }}
{{with .Description }} {{.|prefixLines "// "}} {{end}}
type {{ .Name|go }} struct {
{{- range $field := .Fields }}
{{- with .Description }}
{{.|prefixLines "// "}}
{{- end}}
{{ $field.Name|go }} {{$field.Type | ref}} `{{$field.Tag}} {{$field.Gorm}}`
{{- end }}
}
{{- range $iface := .Implements }}
func ({{ $model.Name|go }}) Is{{ $iface|go }}() {}
{{- end }}
{{- end}}
{{ range $enum := .Enums }}
{{ with .Description }} {{.|prefixLines "// "}} {{end}}
type {{.Name|go }} string
const (
{{- range $value := .Values}}
{{- with .Description}}
{{.|prefixLines "// "}}
{{- end}}
{{ $enum.Name|go }}{{ .Name|go }} {{$enum.Name|go }} = {{.Name|quote}}
{{- end }}
)
var All{{.Name|go }} = []{{ .Name|go }}{
{{- range $value := .Values}}
{{$enum.Name|go }}{{ .Name|go }},
{{- end }}
}
func (e {{.Name|go }}) IsValid() bool {
switch e {
case {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ $enum.Name|go }}{{ $element.Name|go }}{{end}}:
return true
}
return false
}
func (e {{.Name|go }}) String() string {
return string(e)
}
func (e *{{.Name|go }}) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = {{ .Name|go }}(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid {{ .Name }}", str)
}
return nil
}
func (e {{.Name|go }}) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
{{- end }}