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:
3
internal/.gitignore
vendored
3
internal/.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/server.go
|
||||
/graph/generated
|
||||
/graph/generated
|
||||
/model/models_gen.go
|
@ -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
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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
25
internal/graph/helper.go
Normal 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
|
||||
}
|
@ -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"`
|
||||
}
|
7
internal/graph/mutation.graphql
Normal file
7
internal/graph/mutation.graphql
Normal file
@ -0,0 +1,7 @@
|
||||
input ProfileChanges {
|
||||
name: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
updateProfile(changes: ProfileChanges!): User!
|
||||
}
|
20
internal/graph/mutation.resolvers.go
Normal file
20
internal/graph/mutation.resolvers.go
Normal 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 }
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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{}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
package migration
|
||||
|
||||
import "context"
|
||||
|
||||
type Migration interface {
|
||||
Version() string
|
||||
Up(context.Context) error
|
||||
Down(context.Context) error
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
15
internal/model/user.go
Normal 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"`
|
||||
}
|
57
internal/model/user_repository.go
Normal file
57
internal/model/user_repository.go
Normal 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
84
internal/orm/migration.go
Normal 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,
|
||||
}
|
||||
}
|
@ -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
49
internal/orm/provider.go
Normal 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
47
internal/orm/service.go
Normal 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
47
internal/orm/tx.go
Normal 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)
|
||||
}
|
112
internal/orm/version_resolver.go
Normal file
112
internal/orm/version_resolver.go
Normal 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,
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user