diff --git a/api/openapi-spec/openapi.yaml b/api/openapi-spec/openapi.yaml index 23a7aad..3b72608 100644 --- a/api/openapi-spec/openapi.yaml +++ b/api/openapi-spec/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.1.0 +openapi: 3.0.4 info: title: Room Service description: |- diff --git a/go.mod b/go.mod index 270aa80..b1440a0 100644 --- a/go.mod +++ b/go.mod @@ -12,11 +12,16 @@ require ( github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.25.7 gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/mysql v1.5.1 + gorm.io/gorm v1.25.4 ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 6423c5f..1dec637 100644 --- a/go.sum +++ b/go.sum @@ -9,9 +9,15 @@ 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/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +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.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -46,3 +52,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= +gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= +gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= +gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= diff --git a/internal/controller/controller.go b/internal/controller/controller.go index c30e9c6..7796eca 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -1,6 +1,6 @@ package controller -import "github.com/eurofurence/reg-room-service/internal/database" +import "github.com/eurofurence/reg-room-service/internal/repository/database" // Controller is the service interface, which defines // the functions in the service layer of this application diff --git a/internal/database/repository.go b/internal/database/repository.go deleted file mode 100644 index 67b3504..0000000 --- a/internal/database/repository.go +++ /dev/null @@ -1,3 +0,0 @@ -package database - -type Repository interface{} diff --git a/internal/entity/base.go b/internal/entity/base.go new file mode 100644 index 0000000..6f0ab93 --- /dev/null +++ b/internal/entity/base.go @@ -0,0 +1,10 @@ +package entity + +import "time" + +type Base struct { + ID string `gorm:"primaryKey; type:varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;NOT NULL"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time `sql:"index"` +} diff --git a/internal/entity/group.go b/internal/entity/group.go new file mode 100644 index 0000000..72078e5 --- /dev/null +++ b/internal/entity/group.go @@ -0,0 +1,32 @@ +package entity + +// Group is a group of attendees that wish to be assigned to a Room together. +type Group struct { + Base + + // Name is the name of the group + Name string `gorm:"type:varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;NOT NULL;uniqueIndex:room_group_name_uidx"` + + // Flags is a comma-separated list of flags, with both leading and trailing comma. The allowed flags are configuration dependent + Flags string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"` + + // Comments are optional, not processed in any way + Comments string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" testdiff:"ignore"` + + // MaximumSize defaults to a value from service configuration, but we store it here so admins can increase it manually for some groups + MaximumSize uint + + // Owner is the badge number (attendee ID) of the attendee owning the group. Ownership can be passed to another attendee. + Owner uint +} + +// GroupMember associates attendees to a group, either as a member or as an invited member. +type GroupMember struct { + Member + + // GroupID references the group to which the member belongs (or has been invited) + GroupID string `gorm:"type:varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;NOT NULL;index:room_group_member_grpid"` + + // IsInvite is true if the member has been invited, or false if the member has already joined + IsInvite bool +} diff --git a/internal/entity/member.go b/internal/entity/member.go new file mode 100644 index 0000000..9d6ac96 --- /dev/null +++ b/internal/entity/member.go @@ -0,0 +1,18 @@ +package entity + +import "gorm.io/gorm" + +type Member struct { + // This contains ID = the badge number of the attendee (an attendee can only either be in a + // group or invited, and can only ever be in one room at the same time. + gorm.Model + + // Nickname caches the nickname of the attendee + Nickname string `gorm:"type:varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;NOT NULL"` + + // AvatarURL caches the url to obtain the avatar for this attendee, points to an image such as a png or jpg + AvatarURL string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"` + + // Flags is a comma-separated list of flags such as "has_key", with a leading and trailing comma + Flags string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"` +} diff --git a/internal/entity/room.go b/internal/entity/room.go new file mode 100644 index 0000000..ab60ada --- /dev/null +++ b/internal/entity/room.go @@ -0,0 +1,24 @@ +package entity + +type Room struct { + Base + + // Name is the name of the room + Name string `gorm:"type:varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;NOT NULL;uniqueIndex:room_room_name_uidx"` + + // Flags is a comma-separated list of flags, with both leading and trailing comma. The allowed flags are configuration dependent + Flags string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"` + + // Comments are optional, not processed in any way + Comments string `gorm:"type:varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" testdiff:"ignore"` + + // Size is the size of the room + Size uint +} + +type RoomMember struct { + Member + + // RoomID references the room to which the attendee belongs + RoomID string `gorm:"type:varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;NOT NULL;index:room_room_member_roomid"` +} diff --git a/internal/repository/database/dbrepo/constructors.go b/internal/repository/database/dbrepo/constructors.go new file mode 100644 index 0000000..f12b85f --- /dev/null +++ b/internal/repository/database/dbrepo/constructors.go @@ -0,0 +1 @@ +package dbrepo diff --git a/internal/repository/database/interface.go b/internal/repository/database/interface.go new file mode 100644 index 0000000..c2a5af3 --- /dev/null +++ b/internal/repository/database/interface.go @@ -0,0 +1,9 @@ +package database + +import "context" + +type Repository interface { + Open(ctx context.Context) error + Close(ctx context.Context) + Migrate(ctx context.Context) error +} diff --git a/internal/repository/database/mysqldb/implementation.go b/internal/repository/database/mysqldb/implementation.go new file mode 100644 index 0000000..9b51bb2 --- /dev/null +++ b/internal/repository/database/mysqldb/implementation.go @@ -0,0 +1,72 @@ +package mysqldb + +import ( + "context" + aulogging "github.com/StephanHCB/go-autumn-logging" + "github.com/eurofurence/reg-room-service/internal/entity" + "github.com/eurofurence/reg-room-service/internal/repository/database" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + "time" +) + +type MysqlRepository struct { + db *gorm.DB + connectString string + Now func() time.Time +} + +func Create(connectString string) database.Repository { + return &MysqlRepository{ + Now: time.Now, + connectString: connectString, + } +} + +func (r *MysqlRepository) Open(ctx context.Context) error { + gormConfig := gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + TablePrefix: "room_", + }, + Logger: logger.Default.LogMode(logger.Silent), + } + + db, err := gorm.Open(mysql.Open(r.connectString), &gormConfig) + if err != nil { + aulogging.ErrorErrf(ctx, err, "failed to open mysql connection: %s", err.Error()) + return err + } + + sqlDb, err := db.DB() + if err != nil { + aulogging.ErrorErrf(ctx, err, "failed to configure mysql connection: %s", err.Error()) + return err + } + + // see https://making.pusher.com/production-ready-connection-pooling-in-go/ + sqlDb.SetMaxOpenConns(100) + sqlDb.SetMaxIdleConns(50) + sqlDb.SetConnMaxLifetime(time.Minute * 10) + + r.db = db + return nil +} + +func (r *MysqlRepository) Close(_ context.Context) { + // no more db close in gorm v2 +} + +func (r *MysqlRepository) Migrate(ctx context.Context) error { + err := r.db.AutoMigrate( + &entity.Member{}, + &entity.Group{}, + &entity.Room{}, + ) + if err != nil { + aulogging.Logger.NoCtx().Error().WithErr(err).Printf("failed to migrate mysql db: %s", err.Error()) + return err + } + return nil +}