From cb0917d9169c7855fae641d6ff109779eaca9fa1 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Mon, 14 Feb 2022 01:25:36 +0100 Subject: [PATCH] Add projects --- main.go | 58 ++++++++++++++-- migrations/001_create_users.sql | 11 ++-- migrations/002_emails_are_unique_for_user.sql | 6 +- migrations/003_add_projects.sql | 14 ++++ migrations/tern.conf | 2 +- pkg/application/projects/model.go | 31 +++++++++ pkg/application/projects/repository.go | 8 +++ pkg/application/projects/service.go | 59 +++++++++++++++++ pkg/{ => application}/users/model.go | 0 pkg/{ => application}/users/repository.go | 0 pkg/{ => application}/users/service.go | 14 +++- pkg/{ => application}/users/user.go | 0 pkg/db/postgres/projectsRepository.go | 66 +++++++++++++++++++ pkg/db/postgres/usersRepository.go | 16 ++--- 14 files changed, 262 insertions(+), 23 deletions(-) create mode 100644 migrations/003_add_projects.sql create mode 100644 pkg/application/projects/model.go create mode 100644 pkg/application/projects/repository.go create mode 100644 pkg/application/projects/service.go rename pkg/{ => application}/users/model.go (100%) rename pkg/{ => application}/users/repository.go (100%) rename pkg/{ => application}/users/service.go (70%) rename pkg/{ => application}/users/user.go (100%) create mode 100644 pkg/db/postgres/projectsRepository.go diff --git a/main.go b/main.go index cdc2d97..23fec11 100644 --- a/main.go +++ b/main.go @@ -14,9 +14,10 @@ import ( "io/ioutil" "net/http" "os" + "serverctl/pkg/application/projects" + "serverctl/pkg/application/users" "serverctl/pkg/db" "serverctl/pkg/db/postgres" - "serverctl/pkg/users" "time" ) @@ -76,7 +77,7 @@ func BasicAuthMiddleware(l *zap.Logger, us *users.Service) gin.HandlerFunc { c.Next() } } -func setupApi(l *zap.Logger, cc *cache.Cache, us *users.Service) { +func setupApi(l *zap.Logger, cc *cache.Cache, us *users.Service, ps *projects.Service) { l.Info("Setting up serverctl setupApi (using gin)") r := gin.Default() @@ -100,6 +101,53 @@ func setupApi(l *zap.Logger, cc *cache.Cache, us *users.Service) { c.JSON(http.StatusCreated, gin.H{"message": "user has been registered", "userId": createUser}) }) + projectsApi := r.Group("/projects", BasicAuthMiddleware(l, us)) + projectsApi.POST("/", func(c *gin.Context) { + type CreateProjectRequest struct { + Name string `json:"name" binding:"required"` + } + var createProjectRequest CreateProjectRequest + if err := c.BindJSON(&createProjectRequest); err != nil { + return + } + + userId, _ := c.Get("userId") + createProjectId, err := ps.CreateProject(c.Request.Context(), userId.(int), createProjectRequest.Name) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "you have provided invalid input"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "project has been created", "projectId": createProjectId}) + }) + projectsApi.GET("/", func(c *gin.Context) { + userId, _ := c.Get("userId") + + projectsArr, err := ps.Get(c.Request.Context(), userId.(int)) + if err != nil { + l.Warn(err.Error()) + return + } + + type GetProject struct { + Id int `json:"id" binding:"required"` + Name string `json:"name" binding:"required"` + MemberIds []int `json:"memberIds" binding:"required"` + AdminIds []int `json:"adminIds" binding:"required"` + } + + getProject := make([]GetProject, 0) + for _, p := range projectsArr { + getProject = append(getProject, GetProject{ + Id: p.Id, + Name: p.Name, + MemberIds: p.MemberIds, + AdminIds: p.AdminIds, + }) + } + c.JSON(http.StatusOK, getProject) + }) + containers := r.Group("/containers", BasicAuthMiddleware(l, us)) containers.GET("/", func(c *gin.Context) { type container struct { @@ -141,7 +189,7 @@ func setupCache(l *zap.Logger) *cache.Cache { l.Info("Setting up cache") ristrettoCache, err := ristretto.NewCache(&ristretto.Config{ NumCounters: 1000, - MaxCost: 100, + MaxCost: 100_000_000, BufferItems: 64, }) if err != nil { @@ -188,6 +236,8 @@ func main() { database := db.NewClient(logger) usersRepository := postgres.NewUsersRepository(database) usersService := users.NewService(logger, usersRepository, cacheM) + projectsRepository := postgres.NewProjectsRepository(database) + projectsService := projects.NewService(logger, projectsRepository, cacheM) - setupApi(logger, cacheM, usersService) + setupApi(logger, cacheM, usersService, projectsService) } diff --git a/migrations/001_create_users.sql b/migrations/001_create_users.sql index c12ace6..f5c6c0e 100644 --- a/migrations/001_create_users.sql +++ b/migrations/001_create_users.sql @@ -1,9 +1,10 @@ -create table users( - id serial primary key, - email varchar(320) not null, - password_hash varchar(256) not null +create table sctl_user +( + id int GENERATED BY DEFAULT AS IDENTITY primary key, + email varchar(320) not null, + password_hash varchar(256) not null ); ---- create above / drop below ---- -drop table users; +drop table sctl_user; diff --git a/migrations/002_emails_are_unique_for_user.sql b/migrations/002_emails_are_unique_for_user.sql index 1b34e51..be924be 100644 --- a/migrations/002_emails_are_unique_for_user.sql +++ b/migrations/002_emails_are_unique_for_user.sql @@ -1,10 +1,10 @@ -- Write your migrate up statements here -create unique index users_unique_index -on users(email); +create unique index user_email_unique_index +on sctl_user(email); ---- create above / drop below ---- -drop index users_unique_index; +drop index user_email_unique_index; -- Write your migrate down statements here. If this migration is irreversible -- Then delete the separator line above. diff --git a/migrations/003_add_projects.sql b/migrations/003_add_projects.sql new file mode 100644 index 0000000..e80452b --- /dev/null +++ b/migrations/003_add_projects.sql @@ -0,0 +1,14 @@ +-- Write your migrate up statements here + +create table sctl_project +( + id int GENERATED BY DEFAULT AS IDENTITY primary key, + data jsonb NOT NULL +); + +---- create above / drop below ---- + +drop table sctl_project; + +-- Write your migrate down statements here. If this migration is irreversible +-- Then delete the separator line above. diff --git a/migrations/tern.conf b/migrations/tern.conf index 360a244..f5bab91 100644 --- a/migrations/tern.conf +++ b/migrations/tern.conf @@ -31,4 +31,4 @@ sslmode = prefer [data] # Any fields in the data section are available in migration templates -# prefix = foo +prefix = serverctl diff --git a/pkg/application/projects/model.go b/pkg/application/projects/model.go new file mode 100644 index 0000000..ba0aa2b --- /dev/null +++ b/pkg/application/projects/model.go @@ -0,0 +1,31 @@ +package projects + +type Project struct { + Id int + Name string + MemberIds []int + AdminIds []int +} + +func NewProject(id int, name string, memberIds []int, adminIds []int) *Project { + return &Project{ + Id: id, + Name: name, + MemberIds: memberIds, + AdminIds: adminIds, + } +} + +type CreateProject struct { + Name string + MemberIds []int + AdminIds []int +} + +func NewCreateProject(name string, userId int) *CreateProject { + return &CreateProject{ + Name: name, + MemberIds: []int{userId}, + AdminIds: []int{userId}, + } +} diff --git a/pkg/application/projects/repository.go b/pkg/application/projects/repository.go new file mode 100644 index 0000000..2fec40f --- /dev/null +++ b/pkg/application/projects/repository.go @@ -0,0 +1,8 @@ +package projects + +import "context" + +type Repository interface { + Create(ctx context.Context, project *CreateProject) (int, error) + GetForMemberId(ctx context.Context, memberId int) ([]*Project, error) +} diff --git a/pkg/application/projects/service.go b/pkg/application/projects/service.go new file mode 100644 index 0000000..7967158 --- /dev/null +++ b/pkg/application/projects/service.go @@ -0,0 +1,59 @@ +package projects + +import ( + "context" + "fmt" + "github.com/eko/gocache/cache" + "go.uber.org/zap" +) + +type Service struct { + projectsRepository Repository + logger *zap.Logger + cache *cache.Cache +} + +func NewService(logger *zap.Logger, projectsRepository Repository, cache *cache.Cache) *Service { + return &Service{ + logger: logger, + projectsRepository: projectsRepository, + cache: cache} +} + +func (s *Service) CreateProject(ctx context.Context, userId int, name string) (int, error) { + s.logger.Debug("creating project", + zap.String("name", name), + zap.Int("creatorId", userId)) + + projectId, err := s.projectsRepository.Create(ctx, NewCreateProject(name, userId)) + if err != nil { + s.logger.Warn(err.Error()) + return -1, err + } + + _ = s.cache.Delete(fmt.Sprintf("projects_userId_%d", userId)) + + return projectId, nil +} + +func (s *Service) Get(ctx context.Context, userId int) ([]*Project, error) { + s.logger.Debug("getting projects", + zap.Int("userId", userId)) + + loadFunc := func(key interface{}) (interface{}, error) { + s.logger.Debug("getting projects from repository", + zap.Int("userId", userId)) + return s.projectsRepository.GetForMemberId(ctx, userId) + } + + cacheEntry := cache.NewLoadable( + loadFunc, + s.cache) + + entry, err := cacheEntry.Get(fmt.Sprintf("projects_userId_%d", userId)) + if err != nil { + return nil, err + } + + return entry.([]*Project), nil +} diff --git a/pkg/users/model.go b/pkg/application/users/model.go similarity index 100% rename from pkg/users/model.go rename to pkg/application/users/model.go diff --git a/pkg/users/repository.go b/pkg/application/users/repository.go similarity index 100% rename from pkg/users/repository.go rename to pkg/application/users/repository.go diff --git a/pkg/users/service.go b/pkg/application/users/service.go similarity index 70% rename from pkg/users/service.go rename to pkg/application/users/service.go index a8a5fa9..bf7c192 100644 --- a/pkg/users/service.go +++ b/pkg/application/users/service.go @@ -2,6 +2,7 @@ package users import ( "context" + "fmt" "github.com/eko/gocache/cache" "go.uber.org/zap" ) @@ -39,6 +40,15 @@ func (s *Service) Create(email string, password string) (int, error) { } func (s *Service) Authenticate(ctx context.Context, email string, password string) (*User, error) { - user, err := s.repository.GetByEmail(ctx, email, s.passwordHasher.HashPassword(password)) - return user, err + loadFunc := func(key interface{}) (interface{}, error) { + s.logger.Debug("getting user from cache", zap.String("email", email)) + return s.repository.GetByEmail(ctx, email, s.passwordHasher.HashPassword(password)) + } + + get, err := cache.NewLoadable(loadFunc, s.cache).Get(fmt.Sprintf("user_email_%s", email)) + if err != nil { + return nil, err + } + + return get.(*User), nil } diff --git a/pkg/users/user.go b/pkg/application/users/user.go similarity index 100% rename from pkg/users/user.go rename to pkg/application/users/user.go diff --git a/pkg/db/postgres/projectsRepository.go b/pkg/db/postgres/projectsRepository.go new file mode 100644 index 0000000..36b83d4 --- /dev/null +++ b/pkg/db/postgres/projectsRepository.go @@ -0,0 +1,66 @@ +package postgres + +import ( + "context" + "serverctl/pkg/application/projects" + "serverctl/pkg/db" +) + +var _ projects.Repository = &ProjectsRepository{} + +type ProjectsRepository struct { + db *db.Client +} + +func NewProjectsRepository(db *db.Client) projects.Repository { + return &ProjectsRepository{db: db} +} + +type projectData struct { + Name string `json:"name"` + MemberIds []int `json:"memberIds"` + AdminIds []int `json:"adminIds"` +} + +func NewProjectData(project *projects.CreateProject) projectData { + return projectData{ + Name: project.Name, + AdminIds: project.AdminIds, + MemberIds: project.MemberIds, + } +} + +func (p ProjectsRepository) Create(ctx context.Context, project *projects.CreateProject) (int, error) { + conn := p.db.GetConn(ctx) + defer conn.Release() + + var projectId int + err := conn.QueryRow(ctx, "insert into sctl_project(data) values ($1) returning id", NewProjectData(project)).Scan(&projectId) + if err != nil { + return -1, err + } + + return projectId, nil +} + +func (p ProjectsRepository) GetForMemberId(ctx context.Context, memberId int) ([]*projects.Project, error) { + conn := p.db.GetConn(ctx) + defer conn.Release() + + rows, _ := conn.Query(ctx, "select id, data from sctl_project") + projectsArr := make([]*projects.Project, 0) + + for rows.Next() { + var ( + id int + data projectData + ) + err := rows.Scan(&id, &data) + if err != nil { + return nil, err + } + projectsArr = append(projectsArr, projects.NewProject(id, data.Name, data.MemberIds, data.AdminIds)) + } + + return projectsArr, nil +} diff --git a/pkg/db/postgres/usersRepository.go b/pkg/db/postgres/usersRepository.go index a2ee109..bdf995e 100644 --- a/pkg/db/postgres/usersRepository.go +++ b/pkg/db/postgres/usersRepository.go @@ -3,26 +3,26 @@ package postgres import ( "context" "errors" + users2 "serverctl/pkg/application/users" "serverctl/pkg/db" - "serverctl/pkg/users" ) -var _ users.Repository = &usersRepository{} +var _ users2.Repository = &usersRepository{} type usersRepository struct { databasePool *db.Client } -func NewUsersRepository(db *db.Client) users.Repository { +func NewUsersRepository(db *db.Client) users2.Repository { return &usersRepository{db} } -func (u *usersRepository) Create(ctx context.Context, user *users.CreateUser) (int, error) { +func (u *usersRepository) Create(ctx context.Context, user *users2.CreateUser) (int, error) { var userId int conn := u.databasePool.GetConn(ctx) defer conn.Release() - conn.QueryRow(ctx, "INSERT INTO users(email, password_hash) values ($1, $2) RETURNING id", user.Email, user.PasswordHash).Scan(&userId) + conn.QueryRow(ctx, "INSERT INTO sctl_user(email, password_hash) values ($1, $2) RETURNING id", user.Email, user.PasswordHash).Scan(&userId) if userId == 0 { return -1, errors.New("could not insert data into users table") @@ -31,12 +31,12 @@ func (u *usersRepository) Create(ctx context.Context, user *users.CreateUser) (i return userId, nil } -func (u *usersRepository) GetByEmail(ctx context.Context, email string, passwordHash string) (*users.User, error) { +func (u *usersRepository) GetByEmail(ctx context.Context, email string, passwordHash string) (*users2.User, error) { conn := u.databasePool.GetConn(ctx) defer conn.Release() var id int - err := conn.QueryRow(ctx, "select id from users where email = $1 and password_hash = $2", email, passwordHash).Scan(&id) + err := conn.QueryRow(ctx, "select id from sctl_user where email = $1 and password_hash = $2", email, passwordHash).Scan(&id) if err != nil { return nil, err } @@ -45,5 +45,5 @@ func (u *usersRepository) GetByEmail(ctx context.Context, email string, password return nil, errors.New("user with that password doesn't exist") } - return users.NewUser(id, email), nil + return users2.NewUser(id, email), nil }