diff --git a/.env.dist b/.env.dist index 2cb36ba..9c1fec8 100644 --- a/.env.dist +++ b/.env.dist @@ -4,4 +4,4 @@ OIDC_CLIENT_SECRET=daddycool OIDC_POST_LOGOUT_REDIRECT_URL=http://localhost:8081/logout/redirect HTTP_COOKIE_AUTHENTICATION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ HTTP_COOKIE_ENCRYPTION_KEY=cL87ucJJSGt7XSjRuQe7GDb2qna8ijfQ -DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy" \ No newline at end of file +DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy port=5432 sslmode=disable" \ No newline at end of file diff --git a/Makefile b/Makefile index 80a6fac..3143249 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ build-docker: docker-compose build generate: - cd internal && go run github.com/99designs/gqlgen generate + go generate ./... build-server: CGO_ENABLED=0 go build -v -o ./bin/server ./cmd/server diff --git a/README.md b/README.md index 1aae405..6071fc2 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Les services suivants devraient être disponibles après démarrage de l'environ |Service|Type|Accès|Description| |-------|----|-----|-----------| |Application React|HTTP (UI)|http://localhost:8080/|Page d'accueil de l'application React (serveur Webpack)| -|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/api/v1/graphql (GET)|Interface Web de développement de l'API GraphQL (mode debug uniquement, nécessite d'être authentifié)| +|Interface Web GraphQL|HTTP (UI)|http://localhost:8081/api/v1/playground|Interface Web de développement de l'API GraphQL (mode debug uniquement, nécessite d'être authentifié)| |Serveur GraphQL|HTTP (GraphQL)|http://localhost:8081/api/v1/graphql (POST)|Point d'entrée de l'API GraphQL| |Serveur Hydra|HTTP (ReST)|http://localhost:4444|Point d'entrée pour l'API OAuth2 d'[Hydra](https://www.ory.sh/hydra/docs/)| |Serveur Hydra Passwordless|HTTP|http://localhost:3000|Point d'entrée pour la ["Login/Consent App"](https://www.ory.sh/hydra/docs/implementing-consent) [hydra-passwordless](https://forge.cadoles.com/wpetit/hydra-passwordless)| diff --git a/cmd/server/container.go b/cmd/server/container.go index 4984004..c2bb5d1 100644 --- a/cmd/server/container.go +++ b/cmd/server/container.go @@ -4,10 +4,7 @@ import ( "context" "net/http" - "forge.cadoles.com/Cadoles/daddy/internal/migration" - "gitlab.com/wpetit/goweb/cqrs" - - "forge.cadoles.com/Cadoles/daddy/internal/database" + "forge.cadoles.com/Cadoles/daddy/internal/orm" "gitlab.com/wpetit/goweb/logger" @@ -84,21 +81,7 @@ func getServiceContainer(ctx context.Context, conf *config.Config) (*service.Con oidc.WithScopes("email", "openid"), )) - ctn.Provide(database.ServiceName, database.ServiceProvider(conf.Database.DSN)) - - dbpool, err := database.From(ctn) - if err != nil { - return nil, errors.Wrap(err, "could not retrieve database service") - } - - versionResolver := database.NewVersionResolver(dbpool) - if err := versionResolver.Init(ctx); err != nil { - return nil, errors.Wrap(err, "could not initialize database version resolver") - } - - ctn.Provide(migration.ServiceName, migration.ServiceProvider(versionResolver)) - - ctn.Provide(cqrs.ServiceName, cqrs.ServiceProvider()) + ctn.Provide(orm.ServiceName, orm.ServiceProvider("postgres", conf.Database.DSN, conf.Debug)) return ctn, nil } diff --git a/cmd/server/cqrs.go b/cmd/server/cqrs.go deleted file mode 100644 index 875c475..0000000 --- a/cmd/server/cqrs.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "forge.cadoles.com/Cadoles/daddy/internal/command" - "forge.cadoles.com/Cadoles/daddy/internal/query" - "github.com/pkg/errors" - "gitlab.com/wpetit/goweb/cqrs" - "gitlab.com/wpetit/goweb/service" -) - -func initCommands(ctn *service.Container) error { - dispatcher, err := cqrs.From(ctn) - if err != nil { - return errors.WithStack(err) - } - - dispatcher.RegisterCommand( - cqrs.MatchCommandRequest(&command.CreateUserCommandRequest{}), - cqrs.CommandHandlerFunc(command.HandleCreateUserCommand), - ) - - return nil -} - -func initQueries(ctn *service.Container) error { - dispatcher, err := cqrs.From(ctn) - if err != nil { - return errors.WithStack(err) - } - - dispatcher.RegisterQuery( - cqrs.MatchQueryRequest(&query.FindUserQueryRequest{}), - cqrs.QueryHandlerFunc(query.HandleFindUserQuery), - ) - - return nil -} diff --git a/cmd/server/main.go b/cmd/server/main.go index a61c792..0f9c0e1 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -153,23 +153,6 @@ func main() { os.Exit(0) } - // Init commands and queries - if err := initCommands(ctn); err != nil { - logger.Fatal( - ctx, - "could not init commands", - logger.E(err), - ) - } - - if err := initQueries(ctn); err != nil { - logger.Fatal( - ctx, - "could not init queries", - logger.E(err), - ) - } - r := chi.NewRouter() // Define base middlewares diff --git a/cmd/server/migration.go b/cmd/server/migration.go index fedce18..083ee3e 100644 --- a/cmd/server/migration.go +++ b/cmd/server/migration.go @@ -3,9 +3,9 @@ package main import ( "context" - "forge.cadoles.com/Cadoles/daddy/internal/database" - "forge.cadoles.com/Cadoles/daddy/internal/migration" - "github.com/jackc/pgx/v4" + "forge.cadoles.com/Cadoles/daddy/internal/model" + "forge.cadoles.com/Cadoles/daddy/internal/orm" + "github.com/jinzhu/gorm" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/service" @@ -18,11 +18,13 @@ const ( ) func applyMigration(ctx context.Context, ctn *service.Container) error { - migr, err := migration.From(ctn) + orm, err := orm.From(ctn) if err != nil { return err } + migr := orm.Migration() + // Register available migrations migr.Register( m000initialSchema(), @@ -74,29 +76,31 @@ func applyMigration(ctx context.Context, ctn *service.Container) error { return nil } -func m000initialSchema() migration.Migration { - return database.NewMigration( +// nolint: gochecknoglobals +var initialModels = []interface{}{ + &model.User{}, +} + +func m000initialSchema() orm.Migration { + return orm.NewDBMigration( "00_initial_schema", - func(ctx context.Context, tx pgx.Tx) error { - _, err := tx.Exec(ctx, ` - CREATE TABLE users ( - id SERIAL PRIMARY KEY, - name TEXT, - email TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT now(), - connected_at TIMESTAMPTZ, - CONSTRAINT unique_email unique(email) - ); - `) + func(ctx context.Context, tx *gorm.DB) error { + for _, m := range initialModels { + if err := tx.AutoMigrate(m).Error; err != nil { + return errors.WithStack(err) + } + } - return err + return nil }, - func(ctx context.Context, tx pgx.Tx) error { - _, err := tx.Exec(ctx, ` - DROP TABLE users; - `) + func(ctx context.Context, tx *gorm.DB) error { + for _, m := range initialModels { + if err := tx.DropTableIfExists(m).Error; err != nil { + return errors.WithStack(err) + } + } - return err + return nil }, ) } diff --git a/go.mod b/go.mod index 3e5fecd..bc11e1b 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,13 @@ require ( github.com/99designs/gqlgen v0.11.3 github.com/caarlos0/env/v6 v6.2.2 github.com/cortesi/modd v0.0.0-20200630120222-8983974e5450 // indirect + github.com/davecgh/go-spew v1.1.1 github.com/go-chi/chi v4.1.0+incompatible github.com/gorilla/sessions v1.2.0 github.com/gorilla/websocket v1.2.0 github.com/jackc/pgx v3.6.2+incompatible github.com/jackc/pgx/v4 v4.7.1 + github.com/jinzhu/gorm v1.9.14 github.com/pkg/errors v0.9.1 github.com/rs/cors v1.7.0 github.com/vektah/gqlparser/v2 v2.0.1 diff --git a/go.sum b/go.sum index 3f82143..b5c4749 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 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= @@ -42,6 +43,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/bmatcuk/doublestar v1.3.0/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY= @@ -50,6 +52,7 @@ github.com/caarlos0/env/v6 v6.2.2 h1:R0NIFXaB/LhwuGrjnsldzpnVNjFU/U+hTVHt+cq0yDY github.com/caarlos0/env/v6 v6.2.2/go.mod h1:3LpmfcAYCG6gCiSgDLaFR5Km1FRpPwFvBbRcjHar6Sw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= @@ -73,12 +76,16 @@ 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/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= @@ -90,9 +97,14 @@ github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -188,6 +200,12 @@ github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jackc/puddle v1.1.1 h1:PJAw7H/9hoWC4Kf3J8iNmL1SwA6E8vfsLqBiL+F6CtI= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/gorm v1.9.14 h1:Kg3ShyTPcM6nzVo148fRrcMO6MNKuqtOUwnzqMgVniM= +github.com/jinzhu/gorm v1.9.14/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -203,7 +221,9 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 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= @@ -226,6 +246,8 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -260,6 +282,7 @@ 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.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-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 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= @@ -302,6 +325,7 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 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= @@ -309,6 +333,7 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -333,6 +358,7 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc 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.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -347,6 +373,8 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/internal/.gitignore b/internal/.gitignore index 46767d5..9bf1b4d 100644 --- a/internal/.gitignore +++ b/internal/.gitignore @@ -1,2 +1,3 @@ /server.go -/graph/generated \ No newline at end of file +/graph/generated +/model/models_gen.go \ No newline at end of file diff --git a/internal/command/create_user.go b/internal/command/create_user.go deleted file mode 100644 index 82ac4ea..0000000 --- a/internal/command/create_user.go +++ /dev/null @@ -1,99 +0,0 @@ -package command - -import ( - "context" - - "github.com/jackc/pgx/v4/pgxpool" - - "forge.cadoles.com/Cadoles/daddy/internal/database" - - "github.com/pkg/errors" - "gitlab.com/wpetit/goweb/cqrs" - "gitlab.com/wpetit/goweb/middleware/container" -) - -const ( - createConnectedUserStatement = ` - INSERT INTO users (email, connected_at) VALUES ($1, now()) - ON CONFLICT ON CONSTRAINT unique_email - DO UPDATE SET connected_at = now(); - ` - createUserStatement = ` - INSERT INTO users (email) VALUES ($1) - ON CONFLICT ON CONSTRAINT unique_email - DO NOTHING; - ` -) - -type CreateUserCommandRequest struct { - Email string - Connected bool -} - -func HandleCreateUserCommand(ctx context.Context, cmd cqrs.Command) error { - req, ok := cmd.Request().(*CreateUserCommandRequest) - if !ok { - return errors.WithStack(cqrs.ErrUnexpectedRequest) - } - - ctn, err := container.From(ctx) - if err != nil { - return errors.WithStack(err) - } - - pool, err := database.From(ctn) - if err != nil { - return errors.WithStack(err) - } - - conn, err := pool.Acquire(ctx) - if err != nil { - return errors.WithStack(err) - } - - defer conn.Release() - - if req.Connected { - if err := createConnectedUser(ctx, conn, req.Email); err != nil { - return errors.WithStack(err) - } - } else { - if err := createUser(ctx, conn, req.Email); err != nil { - return errors.WithStack(err) - } - } - - return nil -} - -func createConnectedUser(ctx context.Context, conn *pgxpool.Conn, email string) error { - _, err := conn.Conn().Prepare( - ctx, "create_connected_user", - createConnectedUserStatement, - ) - if err != nil { - return errors.WithStack(err) - } - - if _, err := conn.Exec(ctx, "create_connected_user", email); err != nil { - return errors.WithStack(err) - } - - return nil -} - -func createUser(ctx context.Context, conn *pgxpool.Conn, email string) error { - _, err := conn.Conn().Prepare( - ctx, "create_user", - createUserStatement, - ) - if err != nil { - return errors.WithStack(err) - } - - if _, err := conn.Exec(ctx, "create_user", email); err != nil { - return errors.WithStack(err) - } - - return nil -} diff --git a/internal/database/migration.go b/internal/database/migration.go deleted file mode 100644 index e2dcbc9..0000000 --- a/internal/database/migration.go +++ /dev/null @@ -1,79 +0,0 @@ -package database - -import ( - "context" - - "github.com/jackc/pgx/v4" - - "github.com/jackc/pgx/v4/pgxpool" - "github.com/pkg/errors" - "gitlab.com/wpetit/goweb/middleware/container" -) - -type MigrationFunc func(ctx context.Context, tx pgx.Tx) error - -type Migration struct { - version string - up MigrationFunc - down MigrationFunc -} - -func (m *Migration) Version() string { - return m.version -} - -func (m *Migration) Up(ctx context.Context) error { - pool, err := m.getDatabaseService(ctx) - if err != nil { - return err - } - - err = WithTx(ctx, pool, func(ctx context.Context, tx pgx.Tx) error { - return m.up(ctx, tx) - }) - - if err != nil { - return errors.Wrap(err, "could not apply up migration") - } - - return nil -} - -func (m *Migration) Down(ctx context.Context) error { - pool, err := m.getDatabaseService(ctx) - if err != nil { - return err - } - - err = WithTx(ctx, pool, func(ctx context.Context, tx pgx.Tx) error { - return m.down(ctx, tx) - }) - - if err != nil { - return errors.Wrap(err, "could not apply down migration") - } - - return nil -} - -func (m *Migration) getDatabaseService(ctx context.Context) (*pgxpool.Pool, error) { - ctn, err := container.From(ctx) - if err != nil { - return nil, errors.Wrap(err, "could not retrieve service container") - } - - pool, err := From(ctn) - if err != nil { - return nil, errors.Wrap(err, "could not retrieve database service") - } - - return pool, nil -} - -func NewMigration(version string, up, down MigrationFunc) *Migration { - return &Migration{ - version: version, - up: up, - down: down, - } -} diff --git a/internal/database/provider.go b/internal/database/provider.go deleted file mode 100644 index 62c8c89..0000000 --- a/internal/database/provider.go +++ /dev/null @@ -1,24 +0,0 @@ -package database - -import ( - "context" - - "github.com/jackc/pgx/v4/pgxpool" - "github.com/pkg/errors" - "gitlab.com/wpetit/goweb/service" -) - -func ServiceProvider(dsn string) service.Provider { - pool, err := pgxpool.Connect(context.Background(), dsn) - if err != nil { - err = errors.Wrap(err, "could not connect to database") - } - - return func(ctn *service.Container) (interface{}, error) { - if err != nil { - return nil, err - } - - return pool, nil - } -} diff --git a/internal/database/service.go b/internal/database/service.go deleted file mode 100644 index cad5509..0000000 --- a/internal/database/service.go +++ /dev/null @@ -1,34 +0,0 @@ -package database - -import ( - "github.com/jackc/pgx/v4/pgxpool" - "github.com/pkg/errors" - "gitlab.com/wpetit/goweb/service" -) - -const ServiceName service.Name = "database" - -// From retrieves the database pool service in the given container. -func From(container *service.Container) (*pgxpool.Pool, error) { - service, err := container.Service(ServiceName) - if err != nil { - return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName) - } - - srv, ok := service.(*pgxpool.Pool) - if !ok { - return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName) - } - - return srv, nil -} - -// Must retrieves the database pool service in the given container or panic otherwise. -func Must(container *service.Container) *pgxpool.Pool { - srv, err := From(container) - if err != nil { - panic(err) - } - - return srv -} diff --git a/internal/database/tx.go b/internal/database/tx.go deleted file mode 100644 index bafa9ff..0000000 --- a/internal/database/tx.go +++ /dev/null @@ -1,38 +0,0 @@ -package database - -import ( - "context" - - "github.com/jackc/pgx/v4" - "github.com/jackc/pgx/v4/pgxpool" - "github.com/pkg/errors" -) - -func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(context.Context, pgx.Tx) error) error { - tx, err := pool.BeginTx(ctx, pgx.TxOptions{}) - if err != nil { - return errors.Wrap(err, "could not begin transaction") - } - - defer func() { - if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { - panic(errors.Wrap(err, "could not rollback transaction")) - } - }() - - if err := fn(ctx, tx); err != nil { - err := errors.Wrap(err, "could not apply down migration") - - if rollbackErr := tx.Rollback(ctx); rollbackErr != nil { - return errors.Wrap(err, rollbackErr.Error()) - } - - return err - } - - if err := tx.Commit(ctx); err != nil { - return errors.Wrap(err, "could not commit transaction") - } - - return nil -} diff --git a/internal/database/version_resolver.go b/internal/database/version_resolver.go deleted file mode 100644 index eb681b0..0000000 --- a/internal/database/version_resolver.go +++ /dev/null @@ -1,94 +0,0 @@ -package database - -import ( - "context" - - "github.com/jackc/pgx/v4" - "github.com/jackc/pgx/v4/pgxpool" - "github.com/pkg/errors" -) - -type VersionResolver struct { - pool *pgxpool.Pool -} - -func (r *VersionResolver) Current(ctx context.Context) (string, error) { - var version string - - err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error { - err := tx.QueryRow(ctx, `SELECT version FROM database_schema WHERE is_current = true;`). - Scan(&version) - - if errors.Is(err, pgx.ErrNoRows) { - return nil - } - - return err - }) - - if err != nil { - return "", errors.Wrap(err, "could execute version resolver init transaction") - } - - return version, nil -} - -func (r *VersionResolver) Set(ctx context.Context, version string) error { - err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error { - if version != "" { - _, err := tx.Exec(ctx, ` - INSERT INTO database_schema (version, is_current, migrated_at) - VALUES - ( - $1, - true, - now() - ) - ON CONFLICT ON CONSTRAINT unique_version - DO UPDATE SET migrated_at = now(), is_current = true; - `, version) - - if err != nil { - return err - } - } - - _, err := tx.Exec(ctx, ` - UPDATE database_schema SET is_current = false, migrated_at = null WHERE version <> $1; - `, version) - - return err - }) - - if err != nil { - return errors.Wrap(err, "could not update schema version") - } - - return nil -} - -func (r *VersionResolver) Init(ctx context.Context) error { - err := WithTx(ctx, r.pool, func(ctx context.Context, tx pgx.Tx) error { - _, err := tx.Exec(ctx, ` - CREATE TABLE IF NOT EXISTS database_schema( - version TEXT NOT NULL, - migrated_at TIME, - is_current BOOLEAN, - CONSTRAINT unique_version UNIQUE(version) - );`) - - return err - }) - - if err != nil { - return errors.Wrap(err, "could execute version resolver init transaction") - } - - return nil -} - -func NewVersionResolver(pool *pgxpool.Pool) *VersionResolver { - return &VersionResolver{ - pool: pool, - } -} diff --git a/internal/gqlgen.yml b/internal/gqlgen.yml index 053abac..ef0cabf 100644 --- a/internal/gqlgen.yml +++ b/internal/gqlgen.yml @@ -1,6 +1,6 @@ # Where are all the schema files located? globs are supported eg src/**/*.graphqls schema: - - graph/*.graphqls + - graph/*.graphql # Where should the generated server code go? exec: @@ -14,7 +14,7 @@ exec: # Where should any generated models go? model: - filename: graph/model/models_gen.go + filename: model/models_gen.go package: model # Where should the resolver implementations go? @@ -35,7 +35,7 @@ resolver: # 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: - - "forge.cadoles.com/Cadoles/daddy/internal/graph/model" + - "forge.cadoles.com/Cadoles/daddy/internal/model" # This section declares type mapping between the GraphQL and go type systems # diff --git a/internal/graph/helper.go b/internal/graph/helper.go new file mode 100644 index 0000000..73ec676 --- /dev/null +++ b/internal/graph/helper.go @@ -0,0 +1,25 @@ +package graph + +import ( + "context" + + "forge.cadoles.com/Cadoles/daddy/internal/orm" + "github.com/jinzhu/gorm" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/middleware/container" +) + +func getDB(ctx context.Context) (*gorm.DB, error) { + ctn, err := container.From(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + orm, err := orm.From(ctn) + if err != nil { + return nil, errors.WithStack(err) + } + + return orm.DB(), nil +} diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go deleted file mode 100644 index 8448226..0000000 --- a/internal/graph/model/models_gen.go +++ /dev/null @@ -1,14 +0,0 @@ -// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. - -package model - -import ( - "time" -) - -type User struct { - Name *string `json:"name"` - Email string `json:"email"` - ConnectedAt time.Time `json:"connectedAt"` - CreatedAt time.Time `json:"createdAt"` -} diff --git a/internal/graph/mutation.graphql b/internal/graph/mutation.graphql new file mode 100644 index 0000000..3f0f71a --- /dev/null +++ b/internal/graph/mutation.graphql @@ -0,0 +1,7 @@ +input ProfileChanges { + name: String +} + +type Mutation { + updateProfile(changes: ProfileChanges!): User! +} \ No newline at end of file diff --git a/internal/graph/mutation.resolvers.go b/internal/graph/mutation.resolvers.go new file mode 100644 index 0000000..c979e19 --- /dev/null +++ b/internal/graph/mutation.resolvers.go @@ -0,0 +1,20 @@ +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" + + "forge.cadoles.com/Cadoles/daddy/internal/graph/generated" + "forge.cadoles.com/Cadoles/daddy/internal/model" +) + +func (r *mutationResolver) UpdateProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) { + return handleUpdateUserProfile(ctx, changes) +} + +// Mutation returns generated.MutationResolver implementation. +func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } + +type mutationResolver struct{ *Resolver } diff --git a/internal/graph/schema.graphqls b/internal/graph/query.graphql similarity index 66% rename from internal/graph/schema.graphqls rename to internal/graph/query.graphql index 7ae4e20..b11434c 100644 --- a/internal/graph/schema.graphqls +++ b/internal/graph/query.graphql @@ -1,7 +1,3 @@ -# GraphQL schema example -# -# https://gqlgen.com/getting-started/ - scalar Time type User { @@ -13,4 +9,4 @@ type User { type Query { userProfile: User -} \ No newline at end of file +} diff --git a/internal/graph/schema.resolvers.go b/internal/graph/query.resolvers.go similarity index 88% rename from internal/graph/schema.resolvers.go rename to internal/graph/query.resolvers.go index a8eee61..33af4b3 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/query.resolvers.go @@ -7,10 +7,10 @@ import ( "context" "forge.cadoles.com/Cadoles/daddy/internal/graph/generated" - "forge.cadoles.com/Cadoles/daddy/internal/graph/model" + model1 "forge.cadoles.com/Cadoles/daddy/internal/model" ) -func (r *queryResolver) UserProfile(ctx context.Context) (*model.User, error) { +func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) { return handleUserProfile(ctx) } diff --git a/internal/graph/resolver.go b/internal/graph/resolver.go index a25c09c..e06e4a1 100644 --- a/internal/graph/resolver.go +++ b/internal/graph/resolver.go @@ -4,4 +4,6 @@ package graph // // It serves as dependency injection for your app, add any dependencies you require here. +//go:generate go run github.com/99designs/gqlgen + type Resolver struct{} diff --git a/internal/graph/user_profile.go b/internal/graph/user_profile.go index 229fd61..116aa9b 100644 --- a/internal/graph/user_profile.go +++ b/internal/graph/user_profile.go @@ -3,43 +3,33 @@ package graph import ( "context" - "forge.cadoles.com/Cadoles/daddy/internal/graph/model" - "forge.cadoles.com/Cadoles/daddy/internal/query" "forge.cadoles.com/Cadoles/daddy/internal/session" + + "forge.cadoles.com/Cadoles/daddy/internal/model" "github.com/pkg/errors" - "gitlab.com/wpetit/goweb/cqrs" - "gitlab.com/wpetit/goweb/middleware/container" ) func handleUserProfile(ctx context.Context) (*model.User, error) { + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + userEmail, err := session.UserEmail(ctx) if err != nil { return nil, errors.WithStack(err) } - ctn, err := container.From(ctx) + repo := model.NewUserRepository(db) + + user, err := repo.FindUserByEmail(ctx, userEmail) if err != nil { return nil, errors.WithStack(err) } - dispatcher, err := cqrs.From(ctn) - if err != nil { - return nil, errors.WithStack(err) - } - - qry := &query.FindUserQueryRequest{ - Email: userEmail, - } - - result, err := dispatcher.Query(ctx, qry) - if err != nil { - return nil, errors.WithStack(err) - } - - findUserData, ok := result.Data().(*query.FindUserData) - if !ok { - return nil, errors.WithStack(cqrs.ErrUnexpectedData) - } - - return findUserData.User, nil + return user, nil +} + +func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) { + return nil, nil } diff --git a/internal/migration/migration.go b/internal/migration/migration.go deleted file mode 100644 index 9695592..0000000 --- a/internal/migration/migration.go +++ /dev/null @@ -1,9 +0,0 @@ -package migration - -import "context" - -type Migration interface { - Version() string - Up(context.Context) error - Down(context.Context) error -} diff --git a/internal/migration/provider.go b/internal/migration/provider.go deleted file mode 100644 index 0a54705..0000000 --- a/internal/migration/provider.go +++ /dev/null @@ -1,13 +0,0 @@ -package migration - -import ( - "gitlab.com/wpetit/goweb/service" -) - -func ServiceProvider(resolver VersionResolver) service.Provider { - manager := NewManager(resolver) - - return func(ctn *service.Container) (interface{}, error) { - return manager, nil - } -} diff --git a/internal/migration/service.go b/internal/migration/service.go deleted file mode 100644 index 62553ba..0000000 --- a/internal/migration/service.go +++ /dev/null @@ -1,33 +0,0 @@ -package migration - -import ( - "github.com/pkg/errors" - "gitlab.com/wpetit/goweb/service" -) - -const ServiceName service.Name = "migration" - -// From retrieves the migration service in the given container. -func From(container *service.Container) (*Manager, error) { - service, err := container.Service(ServiceName) - if err != nil { - return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName) - } - - srv, ok := service.(*Manager) - if !ok { - return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName) - } - - return srv, nil -} - -// Must retrieves the migration service in the given container or panic otherwise. -func Must(container *service.Container) *Manager { - srv, err := From(container) - if err != nil { - panic(err) - } - - return srv -} diff --git a/internal/migration/version_resolver.go b/internal/migration/version_resolver.go deleted file mode 100644 index 12e05c0..0000000 --- a/internal/migration/version_resolver.go +++ /dev/null @@ -1,8 +0,0 @@ -package migration - -import "context" - -type VersionResolver interface { - Current(context.Context) (string, error) - Set(context.Context, string) error -} diff --git a/internal/model/user.go b/internal/model/user.go new file mode 100644 index 0000000..f2877b5 --- /dev/null +++ b/internal/model/user.go @@ -0,0 +1,15 @@ +package model + +import "time" + +type User struct { + ID *uint `gorm:"primary_key"` + Name *string `json:"name"` + Email string `json:"email" gorm:"unique;not null"` + ConnectedAt time.Time `json:"connectedAt"` + CreatedAt time.Time `json:"createdAt"` +} + +type ProfileChanges struct { + Name *string `json:"name"` +} diff --git a/internal/model/user_repository.go b/internal/model/user_repository.go new file mode 100644 index 0000000..ec246d7 --- /dev/null +++ b/internal/model/user_repository.go @@ -0,0 +1,57 @@ +package model + +import ( + "context" + "time" + + "forge.cadoles.com/Cadoles/daddy/internal/orm" + "github.com/jinzhu/gorm" + "github.com/pkg/errors" +) + +type UserRepository struct { + db *gorm.DB +} + +func (r *UserRepository) CreateOrConnectUser(ctx context.Context, email string) (*User, error) { + user := &User{ + Email: email, + CreatedAt: time.Now(), + } + + err := orm.WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error { + err := tx.Where("email = ?", email).FirstOrCreate(user).Error + if err != nil { + return errors.WithStack(err) + } + + if err := tx.Model(user).UpdateColumn("connected_at", time.Now()).Error; err != nil { + return errors.WithStack(err) + } + + return nil + }) + + if err != nil { + return nil, errors.Wrap(err, "could not create user") + } + + return user, nil +} + +func (r *UserRepository) FindUserByEmail(ctx context.Context, email string) (*User, error) { + user := &User{ + Email: email, + } + + err := r.db.First(user, "email = ?", email).Error + if err != nil { + return nil, errors.Wrap(err, "could not find user") + } + + return user, nil +} + +func NewUserRepository(db *gorm.DB) *UserRepository { + return &UserRepository{db} +} diff --git a/internal/orm/migration.go b/internal/orm/migration.go new file mode 100644 index 0000000..a1aa902 --- /dev/null +++ b/internal/orm/migration.go @@ -0,0 +1,84 @@ +package orm + +import ( + "context" + + "github.com/jinzhu/gorm" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/middleware/container" +) + +type MigrationFunc func(ctx context.Context, tx *gorm.DB) error + +type Migration interface { + Version() string + Up(context.Context) error + Down(context.Context) error +} + +type DBMigration struct { + version string + up MigrationFunc + down MigrationFunc +} + +func (m *DBMigration) Version() string { + return m.version +} + +func (m *DBMigration) Up(ctx context.Context) error { + db, err := m.getDatabase(ctx) + if err != nil { + return err + } + + err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error { + return m.up(ctx, tx) + }) + + if err != nil { + return errors.Wrap(err, "could not apply up migration") + } + + return nil +} + +func (m *DBMigration) Down(ctx context.Context) error { + db, err := m.getDatabase(ctx) + if err != nil { + return err + } + + err = WithTx(ctx, db, func(ctx context.Context, tx *gorm.DB) error { + return m.down(ctx, tx) + }) + + if err != nil { + return errors.Wrap(err, "could not apply down migration") + } + + return nil +} + +func (m *DBMigration) getDatabase(ctx context.Context) (*gorm.DB, error) { + ctn, err := container.From(ctx) + if err != nil { + return nil, errors.Wrap(err, "could not retrieve service container") + } + + orm, err := From(ctn) + if err != nil { + return nil, errors.Wrap(err, "could not retrieve orm service") + } + + return orm.DB(), nil +} + +func NewDBMigration(version string, up, down MigrationFunc) *DBMigration { + return &DBMigration{ + version: version, + up: up, + down: down, + } +} diff --git a/internal/migration/manager.go b/internal/orm/migration_manager.go similarity index 81% rename from internal/migration/manager.go rename to internal/orm/migration_manager.go index 91de119..4f72dda 100644 --- a/internal/migration/manager.go +++ b/internal/orm/migration_manager.go @@ -1,4 +1,4 @@ -package migration +package orm import ( "context" @@ -11,12 +11,12 @@ var ( ErrMigrationNotFound = errors.New("migration not found") ) -type Manager struct { +type MigrationManager struct { migrations []Migration resolver VersionResolver } -func (m *Manager) Up(ctx context.Context) error { +func (m *MigrationManager) Up(ctx context.Context) error { currentVersion, err := m.resolver.Current(ctx) if err != nil { return errors.Wrap(err, "could not retrieve current version") @@ -58,7 +58,7 @@ func (m *Manager) Up(ctx context.Context) error { return errors.WithStack(ErrMigrationNotFound) } -func (m *Manager) Down(ctx context.Context) error { +func (m *MigrationManager) Down(ctx context.Context) error { currentVersion, err := m.resolver.Current(ctx) if err != nil { return errors.Wrap(err, "could not retrieve current version") @@ -91,7 +91,7 @@ func (m *Manager) Down(ctx context.Context) error { return errors.WithStack(ErrMigrationNotFound) } -func (m *Manager) Latest(ctx context.Context) error { +func (m *MigrationManager) Latest(ctx context.Context) error { for { isLatest, err := m.IsLatest(ctx) if err != nil { @@ -108,15 +108,15 @@ func (m *Manager) Latest(ctx context.Context) error { } } -func (m *Manager) Register(migrations ...Migration) { +func (m *MigrationManager) Register(migrations ...Migration) { m.migrations = migrations } -func (m *Manager) CurrentVersion(ctx context.Context) (string, error) { +func (m *MigrationManager) CurrentVersion(ctx context.Context) (string, error) { return m.resolver.Current(ctx) } -func (m *Manager) LatestVersion() (string, error) { +func (m *MigrationManager) LatestVersion() (string, error) { if len(m.migrations) == 0 { return "", errors.WithStack(ErrNoAvailableMigration) } @@ -124,7 +124,7 @@ func (m *Manager) LatestVersion() (string, error) { return m.migrations[len(m.migrations)-1].Version(), nil } -func (m *Manager) IsLatest(ctx context.Context) (bool, error) { +func (m *MigrationManager) IsLatest(ctx context.Context) (bool, error) { currentVersion, err := m.resolver.Current(ctx) if err != nil { return false, errors.Wrap(err, "could not retrieve current version") @@ -138,8 +138,8 @@ func (m *Manager) IsLatest(ctx context.Context) (bool, error) { return currentVersion == latestVersion, nil } -func NewManager(resolver VersionResolver) *Manager { - return &Manager{ +func NewMigrationManager(resolver VersionResolver) *MigrationManager { + return &MigrationManager{ resolver: resolver, migrations: make([]Migration, 0), } diff --git a/internal/orm/provider.go b/internal/orm/provider.go new file mode 100644 index 0000000..6e946f9 --- /dev/null +++ b/internal/orm/provider.go @@ -0,0 +1,49 @@ +package orm + +import ( + "context" + + "github.com/jinzhu/gorm" + + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/service" + + // Import postgres dialect + _ "github.com/jinzhu/gorm/dialects/postgres" +) + +func ServiceProvider(dialect, dsn string, debug bool) service.Provider { + db, err := gorm.Open(dialect, dsn) + if err != nil { + err = errors.Wrap(err, "could not connect to database") + } + + var srv *Service + + if err == nil { + db = db.LogMode(debug) + + versionResolver := NewDBVersionResolver(db) + ctx := context.Background() + + err := versionResolver.Init(ctx) + if err != nil { + err = errors.Wrap(err, "could not initialize version resolver") + } + + if err == nil { + srv = &Service{ + db: db, + migration: NewMigrationManager(versionResolver), + } + } + } + + return func(ctn *service.Container) (interface{}, error) { + if err != nil { + return nil, err + } + + return srv, nil + } +} diff --git a/internal/orm/service.go b/internal/orm/service.go new file mode 100644 index 0000000..780a817 --- /dev/null +++ b/internal/orm/service.go @@ -0,0 +1,47 @@ +package orm + +import ( + "github.com/jinzhu/gorm" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/service" +) + +const ServiceName service.Name = "orm" + +type Service struct { + db *gorm.DB + migration *MigrationManager +} + +func (s *Service) DB() *gorm.DB { + return s.db +} + +func (s *Service) Migration() *MigrationManager { + return s.migration +} + +// From retrieves the orm service in the given container. +func From(container *service.Container) (*Service, error) { + service, err := container.Service(ServiceName) + if err != nil { + return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName) + } + + srv, ok := service.(*Service) + if !ok { + return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName) + } + + return srv, nil +} + +// Must retrieves the orm pool service in the given container or panic otherwise. +func Must(container *service.Container) *Service { + srv, err := From(container) + if err != nil { + panic(err) + } + + return srv +} diff --git a/internal/orm/tx.go b/internal/orm/tx.go new file mode 100644 index 0000000..c33e7ae --- /dev/null +++ b/internal/orm/tx.go @@ -0,0 +1,47 @@ +package orm + +import ( + "context" + "database/sql" + + "github.com/jinzhu/gorm" + "github.com/pkg/errors" +) + +func WithTx(ctx context.Context, db *gorm.DB, fn func(context.Context, *gorm.DB) error) error { + tx := db.BeginTx(ctx, &sql.TxOptions{}) + + defer func() { + if err := tx.Rollback().Error; err != nil && !isGormError(err, gorm.ErrInvalidTransaction) { + panic(errors.Wrap(err, "could not rollback transaction")) + } + }() + + if err := fn(ctx, tx); err != nil { + err := errors.Wrap(err, "could not apply down migration") + + if rollbackErr := tx.Rollback().Error; rollbackErr != nil { + return errors.Wrap(err, rollbackErr.Error()) + } + + return err + } + + if err := tx.Commit().Error; err != nil { + return errors.Wrap(err, "could not commit transaction") + } + + return nil +} + +func isGormError(err error, compErr error) bool { + if errs, ok := err.(gorm.Errors); ok { + for _, err := range errs { + if errors.Is(err, compErr) { + return true + } + } + } + + return errors.Is(err, compErr) +} diff --git a/internal/orm/version_resolver.go b/internal/orm/version_resolver.go new file mode 100644 index 0000000..34ff90b --- /dev/null +++ b/internal/orm/version_resolver.go @@ -0,0 +1,112 @@ +package orm + +import ( + "context" + "time" + + "github.com/jinzhu/gorm" + "github.com/pkg/errors" +) + +type VersionResolver interface { + Current(context.Context) (string, error) + Set(context.Context, string) error +} + +type DBVersionResolver struct { + db *gorm.DB +} + +type DatabaseVersion struct { + ID uint `gorm:"primary_key"` + Version string `gorm:"unique; not null"` + MigratedAt time.Time + IsCurrent bool +} + +func (r *DBVersionResolver) Current(ctx context.Context) (string, error) { + var version string + + err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error { + dbVersion := &DatabaseVersion{} + err := tx.Where("is_current = ?", true).First(dbVersion).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + + if err != nil { + return errors.WithStack(err) + } + + version = dbVersion.Version + + return nil + }) + + if err != nil { + return "", errors.Wrap(err, "could execute version resolver init transaction") + } + + return version, nil +} + +func (r *DBVersionResolver) Set(ctx context.Context, version string) error { + err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error { + dbVersion := &DatabaseVersion{ + Version: version, + MigratedAt: time.Now(), + } + + if version != "" { + if err := tx.FirstOrCreate(dbVersion).Error; err != nil { + return err + } + + err := tx.Model(dbVersion). + UpdateColumn("is_current", true).Error + if err != nil { + return errors.WithStack(err) + } + } + + err := tx.Model(&DatabaseVersion{}). + Where("version <> ?", version). + UpdateColumn("is_current", false).Error + + if err != nil { + return errors.WithStack(err) + } + + return err + }) + + if err != nil { + return errors.Wrap(err, "could not update schema version") + } + + return nil +} + +func (r *DBVersionResolver) Init(ctx context.Context) error { + err := WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error { + if err := tx.AutoMigrate(&DatabaseVersion{}).Error; err != nil { + return errors.WithStack(err) + } + if err := tx.Model(&DatabaseVersion{}).AddUniqueIndex("idx_unique_version", "version").Error; err != nil { + return errors.WithStack(err) + } + return nil + }) + + if err != nil { + return errors.Wrap(err, "could execute version resolver init transaction") + } + + return nil +} + +func NewDBVersionResolver(db *gorm.DB) *DBVersionResolver { + return &DBVersionResolver{ + db: db, + } +} diff --git a/internal/query/find_user.go b/internal/query/find_user.go deleted file mode 100644 index 7212754..0000000 --- a/internal/query/find_user.go +++ /dev/null @@ -1,71 +0,0 @@ -package query - -import ( - "context" - - "forge.cadoles.com/Cadoles/daddy/internal/graph/model" - - "forge.cadoles.com/Cadoles/daddy/internal/database" - - "github.com/pkg/errors" - "gitlab.com/wpetit/goweb/cqrs" - "gitlab.com/wpetit/goweb/middleware/container" -) - -const ( - findUserStatement = `SELECT email, connected_at, created_at FROM users WHERE email = $1` -) - -type FindUserQueryRequest struct { - Email string -} - -type FindUserData struct { - User *model.User -} - -func HandleFindUserQuery(ctx context.Context, qry cqrs.Query) (interface{}, error) { - req, ok := qry.Request().(*FindUserQueryRequest) - if !ok { - return nil, errors.WithStack(cqrs.ErrUnexpectedRequest) - } - - ctn, err := container.From(ctx) - if err != nil { - return nil, errors.WithStack(err) - } - - pool, err := database.From(ctn) - if err != nil { - return nil, errors.WithStack(err) - } - - conn, err := pool.Acquire(ctx) - if err != nil { - return nil, errors.WithStack(err) - } - - defer conn.Release() - - _, err = conn.Conn().Prepare( - ctx, "find_user", - findUserStatement, - ) - if err != nil { - return nil, errors.WithStack(err) - } - - user := &model.User{} - - err = conn.QueryRow(ctx, "find_user", req.Email). - Scan(&user.Email, &user.ConnectedAt, &user.CreatedAt) - if err != nil { - return nil, errors.WithStack(err) - } - - data := &FindUserData{ - User: user, - } - - return data, nil -} diff --git a/internal/route/login.go b/internal/route/login.go index 6db2c4b..e8ec203 100644 --- a/internal/route/login.go +++ b/internal/route/login.go @@ -3,8 +3,8 @@ package route import ( "net/http" - "forge.cadoles.com/Cadoles/daddy/internal/command" - "gitlab.com/wpetit/goweb/cqrs" + "forge.cadoles.com/Cadoles/daddy/internal/model" + "forge.cadoles.com/Cadoles/daddy/internal/orm" "forge.cadoles.com/Cadoles/daddy/internal/session" @@ -62,15 +62,11 @@ func handleLoginCallback(w http.ResponseWriter, r *http.Request) { return } - dispatcher := cqrs.Must(ctn) + db := orm.Must(ctn).DB() + repo := model.NewUserRepository(db) - cmd := &command.CreateUserCommandRequest{ - Email: claims.Email, - Connected: true, - } - - if _, err := dispatcher.Exec(ctx, cmd); err != nil { - panic(errors.WithStack(err)) + if _, err := repo.CreateOrConnectUser(ctx, claims.Email); err != nil { + panic(errors.Wrap(err, "could not upsert user")) } if err := session.SaveUserEmail(w, r, claims.Email); err != nil { diff --git a/modd.conf b/modd.conf index ac44863..9ca8a4e 100644 --- a/modd.conf +++ b/modd.conf @@ -1,4 +1,4 @@ -internal/graph/schema.graphqls +internal/graph/*.graphql internal/gqlgen.yml { prep: make generate }