Bascule sur l'ORM GORM

- On n'utilise plus la pattern CQRS trop lourde pour le système
- Un système de models/repository "à la Symfony" est utilisé pour les
  requêtes
This commit is contained in:
wpetit 2020-07-16 09:28:27 +02:00
parent 8b8f322630
commit 05dd505d6b
40 changed files with 568 additions and 673 deletions

View File

@ -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"
DATABASE_DSN="host=localhost user=daddy database=daddy password=daddy port=5432 sslmode=disable"

View File

@ -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

View File

@ -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)|

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
},
)
}

2
go.mod
View File

@ -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

28
go.sum
View File

@ -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=

1
internal/.gitignore vendored
View File

@ -1,2 +1,3 @@
/server.go
/graph/generated
/model/models_gen.go

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -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
#

25
internal/graph/helper.go Normal file
View File

@ -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
}

View File

@ -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"`
}

View File

@ -0,0 +1,7 @@
input ProfileChanges {
name: String
}
type Mutation {
updateProfile(changes: ProfileChanges!): User!
}

View File

@ -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 }

View File

@ -1,7 +1,3 @@
# GraphQL schema example
#
# https://gqlgen.com/getting-started/
scalar Time
type User {

View File

@ -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)
}

View File

@ -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{}

View File

@ -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)
return user, nil
}
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
func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) {
return nil, nil
}

View File

@ -1,9 +0,0 @@
package migration
import "context"
type Migration interface {
Version() string
Up(context.Context) error
Down(context.Context) error
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -1,8 +0,0 @@
package migration
import "context"
type VersionResolver interface {
Current(context.Context) (string, error)
Set(context.Context, string) error
}

15
internal/model/user.go Normal file
View File

@ -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"`
}

View File

@ -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}
}

84
internal/orm/migration.go Normal file
View File

@ -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,
}
}

View File

@ -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),
}

49
internal/orm/provider.go Normal file
View File

@ -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
}
}

47
internal/orm/service.go Normal file
View File

@ -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
}

47
internal/orm/tx.go Normal file
View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -1,4 +1,4 @@
internal/graph/schema.graphqls
internal/graph/*.graphql
internal/gqlgen.yml {
prep: make generate
}