Skip to content

Commit 98c30bd

Browse files
authored
Merge branch 'main' into add-pinned-stories
2 parents c032d02 + 9cf9c98 commit 98c30bd

File tree

14 files changed

+208
-54
lines changed

14 files changed

+208
-54
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jobs:
5454
make db_create
5555
make db_migrate
5656
make test
57+
make db_drop db_create db_migrate
5758
make coverage
5859
- name: Coveralls
5960
uses: coverallsapp/github-action@v2

controller/users/create.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package users
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/sirupsen/logrus"
9+
"github.com/source-academy/stories-backend/controller"
10+
"github.com/source-academy/stories-backend/internal/database"
11+
apierrors "github.com/source-academy/stories-backend/internal/errors"
12+
"github.com/source-academy/stories-backend/model"
13+
userparams "github.com/source-academy/stories-backend/params/users"
14+
userviews "github.com/source-academy/stories-backend/view/users"
15+
)
16+
17+
func HandleCreate(w http.ResponseWriter, r *http.Request) error {
18+
var params userparams.Create
19+
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
20+
e, ok := err.(*json.UnmarshalTypeError)
21+
if !ok {
22+
logrus.Error(err)
23+
return apierrors.ClientBadRequestError{
24+
Message: fmt.Sprintf("Bad JSON parsing: %v", err),
25+
}
26+
}
27+
28+
// TODO: Investigate if we should use errors.Wrap instead
29+
return apierrors.ClientUnprocessableEntityError{
30+
Message: fmt.Sprintf("Invalid JSON format: %s should be a %s.", e.Field, e.Type),
31+
}
32+
}
33+
34+
err := params.Validate()
35+
if err != nil {
36+
logrus.Error(err)
37+
return apierrors.ClientUnprocessableEntityError{
38+
Message: fmt.Sprintf("JSON validation failed: %v", err),
39+
}
40+
}
41+
42+
userModel := *params.ToModel()
43+
44+
// Get DB instance
45+
db, err := database.GetDBFrom(r)
46+
if err != nil {
47+
logrus.Error(err)
48+
return err
49+
}
50+
51+
err = model.CreateUser(db, &userModel)
52+
if err != nil {
53+
logrus.Error(err)
54+
return err
55+
}
56+
57+
controller.EncodeJSONResponse(w, userviews.SingleFrom(userModel))
58+
w.WriteHeader(http.StatusCreated)
59+
return nil
60+
}
61+
62+
func HandleBatchCreate(w http.ResponseWriter, r *http.Request) error {
63+
var usersparams userparams.BatchCreate
64+
if err := json.NewDecoder(r.Body).Decode(&usersparams); err != nil {
65+
e, ok := err.(*json.UnmarshalTypeError)
66+
if !ok {
67+
logrus.Error(err)
68+
return apierrors.ClientBadRequestError{
69+
Message: fmt.Sprintf("Bad JSON parsing: %v", err),
70+
}
71+
}
72+
73+
// TODO: Investigate if we should use errors.Wrap instead
74+
return apierrors.ClientUnprocessableEntityError{
75+
Message: fmt.Sprintf("Invalid JSON format: %s should be a %s.", e.Field, e.Type),
76+
}
77+
}
78+
79+
for _, userparams := range usersparams.Users {
80+
err := userparams.Validate()
81+
if err != nil {
82+
logrus.Error(err)
83+
return apierrors.ClientUnprocessableEntityError{
84+
Message: fmt.Sprintf("JSON validation failed: %v", err),
85+
}
86+
}
87+
}
88+
89+
userModels := make([]*model.User, len(usersparams.Users))
90+
for i, userparams := range usersparams.Users {
91+
userModels[i] = userparams.ToModel()
92+
}
93+
94+
// Get DB instance
95+
db, err := database.GetDBFrom(r)
96+
if err != nil {
97+
logrus.Error(err)
98+
return err
99+
}
100+
101+
numCreated, err := model.CreateUsers(db, &userModels)
102+
if err != nil {
103+
logrus.Error(err)
104+
return err
105+
}
106+
107+
controller.EncodeJSONResponse(w, userviews.BatchCreateFrom(userModels, numCreated))
108+
w.WriteHeader(http.StatusCreated)
109+
return nil
110+
}

controller/users/users.go

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package users
22

33
import (
4-
"encoding/json"
54
"fmt"
65
"net/http"
76

@@ -14,7 +13,6 @@ import (
1413
"github.com/source-academy/stories-backend/internal/database"
1514
apierrors "github.com/source-academy/stories-backend/internal/errors"
1615
"github.com/source-academy/stories-backend/model"
17-
userparams "github.com/source-academy/stories-backend/params/users"
1816
userviews "github.com/source-academy/stories-backend/view/users"
1917
)
2018

@@ -60,48 +58,3 @@ func HandleRead(w http.ResponseWriter, r *http.Request) error {
6058
controller.EncodeJSONResponse(w, userviews.SingleFrom(user))
6159
return nil
6260
}
63-
64-
func HandleCreate(w http.ResponseWriter, r *http.Request) error {
65-
var params userparams.Create
66-
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
67-
e, ok := err.(*json.UnmarshalTypeError)
68-
if !ok {
69-
logrus.Error(err)
70-
return apierrors.ClientBadRequestError{
71-
Message: fmt.Sprintf("Bad JSON parsing: %v", err),
72-
}
73-
}
74-
75-
// TODO: Investigate if we should use errors.Wrap instead
76-
return apierrors.ClientUnprocessableEntityError{
77-
Message: fmt.Sprintf("Invalid JSON format: %s should be a %s.", e.Field, e.Type),
78-
}
79-
}
80-
81-
err := params.Validate()
82-
if err != nil {
83-
logrus.Error(err)
84-
return apierrors.ClientUnprocessableEntityError{
85-
Message: fmt.Sprintf("JSON validation failed: %v", err),
86-
}
87-
}
88-
89-
userModel := *params.ToModel()
90-
91-
// Get DB instance
92-
db, err := database.GetDBFrom(r)
93-
if err != nil {
94-
logrus.Error(err)
95-
return err
96-
}
97-
98-
err = model.CreateUser(db, &userModel)
99-
if err != nil {
100-
logrus.Error(err)
101-
return err
102-
}
103-
104-
controller.EncodeJSONResponse(w, userviews.SingleFrom(userModel))
105-
w.WriteHeader(http.StatusCreated)
106-
return nil
107-
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/go-chi/chi v1.5.4
99
github.com/go-chi/chi/v5 v5.0.8
1010
github.com/go-chi/cors v1.2.1
11+
github.com/jackc/pgx/v5 v5.4.1
1112
github.com/joho/godotenv v1.5.1
1213
github.com/lestrrat-go/jwx/v2 v2.0.11
1314
github.com/rubenv/sql-migrate v1.5.1
@@ -25,7 +26,6 @@ require (
2526
github.com/goccy/go-json v0.10.2 // indirect
2627
github.com/jackc/pgpassfile v1.0.0 // indirect
2728
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
28-
github.com/jackc/pgx/v5 v5.4.1 // indirect
2929
github.com/jinzhu/inflection v1.0.0 // indirect
3030
github.com/jinzhu/now v1.1.5 // indirect
3131
github.com/lestrrat-go/blackmagic v1.0.1 // indirect

internal/database/errors.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,38 @@ import (
44
"errors"
55
"fmt"
66

7+
"github.com/jackc/pgx/v5/pgconn"
78
apierrors "github.com/source-academy/stories-backend/internal/errors"
89
"gorm.io/gorm"
910
)
1011

12+
var (
13+
errUniqueIndexViolation = pgconn.PgError{
14+
Code: "23505",
15+
}
16+
)
17+
18+
// Checks if err is a *pgconn.PgError with the given errcode.
19+
// If yes, returns the error as *pgconn.PgError and true.
20+
// Otherwise, returns _ (may be nil or a valid pointer) and false.
21+
func isPGError(err error, errcode pgconn.PgError) (*pgconn.PgError, bool) {
22+
// fmt.Println(errcode.Name())
23+
var pgerr *pgconn.PgError
24+
ok := errors.As(err, &pgerr)
25+
return pgerr, ok && pgerr.Code == errcode.Code
26+
}
27+
1128
func HandleDBError(err error, fromModel string) error {
1229
if errors.Is(err, gorm.ErrRecordNotFound) {
1330
return apierrors.ClientNotFoundError{
1431
Message: fmt.Sprintf("Cannot find requested %s.", fromModel),
1532
}
1633
}
34+
if pgErr, ok := isPGError(err, errUniqueIndexViolation); ok {
35+
return apierrors.ClientConflictError{
36+
Message: fmt.Sprintf("%s %s", fromModel, pgErr.Detail),
37+
}
38+
}
1739
// TODO: Handle more types of errors
1840
return err
1941
}

internal/errors/409.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package apierrors
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
type ClientConflictError struct {
8+
Message string
9+
}
10+
11+
func (e ClientConflictError) Error() string {
12+
return e.Message
13+
}
14+
15+
func (e ClientConflictError) HTTPStatusCode() int {
16+
return http.StatusConflict
17+
}

internal/router/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func Setup(config *config.Config, injectMiddleWares []func(http.Handler) http.Ha
4646
r.Get("/", handleAPIError(users.HandleList))
4747
r.Get("/{userID}", handleAPIError(users.HandleRead))
4848
r.Post("/", handleAPIError(users.HandleCreate))
49+
r.Post("/batch", handleAPIError(users.HandleBatchCreate))
4950
})
5051
})
5152

migrations/20230721215616-make_users_provider_agnostic.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
-- Makes the users table agnostic to the login provider.
22

33
-- +migrate Up
4+
45
CREATE DOMAIN login_provider_type INTEGER NOT NULL;
56

67
ALTER TABLE users
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- +migrate Up
2+
3+
CREATE UNIQUE INDEX idx_unique_username_provider ON users (username, login_provider);
4+
5+
-- +migrate Down
6+
7+
DROP INDEX idx_unique_username_provider;

model/stories_test.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package model
22

33
import (
44
"fmt"
5+
"math/rand"
56
"testing"
67

8+
userenums "github.com/source-academy/stories-backend/internal/enums/users"
79
"github.com/stretchr/testify/assert"
810
)
911

@@ -37,8 +39,8 @@ func TestCreateStory(t *testing.T) {
3739

3840
// We need to first create a user due to the foreign key constraint
3941
user := User{
40-
Username: "testUsername",
41-
LoginProvider: 123,
42+
Username: "testStoryAuthor",
43+
LoginProvider: userenums.LoginProvider(rand.Int31()),
4244
}
4345
_ = CreateUser(db, &user)
4446

@@ -67,10 +69,17 @@ func TestGetStoryByID(t *testing.T) {
6769
db, cleanUp := setupDBConnection(t, dbConfig)
6870
defer cleanUp(t)
6971

72+
// We need to first create a user due to the foreign key constraint
73+
user := User{
74+
Username: "testMultipleStoriesAuthor",
75+
LoginProvider: userenums.LoginProvider(rand.Int31()),
76+
}
77+
_ = CreateUser(db, &user)
78+
7079
stories := []*Story{
71-
{AuthorID: 1, Content: "The quick"},
72-
{AuthorID: 1, Content: "brown fox"},
73-
{AuthorID: 1, Content: "jumps over"},
80+
{AuthorID: user.ID, Content: "The quick"},
81+
{AuthorID: user.ID, Content: "brown fox"},
82+
{AuthorID: user.ID, Content: "jumps over"},
7483
}
7584

7685
for _, storyToAdd := range stories {

0 commit comments

Comments
 (0)