Compare commits

..

No commits in common. "a130a826bc61c31cee8f95c632e235b9ae4d5fcb" and "7add2af0c250c1dcf153f49b5a93588360193e0a" have entirely different histories.

11 changed files with 51 additions and 202 deletions

View File

@ -12,7 +12,6 @@ import (
"git.nochill.in/nochill/hiling_go/util" "git.nochill.in/nochill/hiling_go/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lib/pq" "github.com/lib/pq"
ysqlc "github.com/yiplee/sqlc"
) )
type createLocationReq struct { type createLocationReq struct {
@ -61,20 +60,10 @@ func (server *Server) createLocation(ctx *gin.Context) {
} }
err := server.Store.CreateLocation(ctx, arg) err := server.Store.CreateLocation(ctx, arg)
if err != nil { if err != nil {
if pqErr, ok := err.(*pq.Error); ok { if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() { ctx.JSON(http.StatusConflict, ErrorResponse(err, fmt.Sprintf("Something went wrong, code: %s", pqErr.Code.Name())))
case "foreign_key_violation", "unique_violation": return
if pqErr.Constraint == "locations_regency_id_fkey" {
ctx.JSON(http.StatusConflict, ErrorResponse(err, fmt.Sprintf("Failed to submit location, there's no regency with id: %d", arg.RegencyID)))
return
}
if pqErr.Constraint == "submitted_by_fkey" {
ctx.JSON(http.StatusConflict, ErrorResponse(err, fmt.Sprintf("Failed to submit location, there's no user with id: %d", arg.SubmittedBy)))
return
}
}
} }
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
return return
@ -96,12 +85,12 @@ func (server *Server) getListLocations(ctx *gin.Context) {
return return
} }
locations, err := server.Store.GetListLocations(ysqlc.Build(ctx, func(builder *ysqlc.Builder) { arg := db.GetListLocationsParams{
builder.Limit(int(req.PageSize)) Limit: req.PageSize,
builder.Offset(int(req.Page-1) * int(req.PageSize)) Offset: (req.Page - 1) * req.PageSize,
builder.Order("created_at ASC") }
}))
locations, err := server.Store.GetListLocations(ctx, arg)
if err != nil { if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
return return
@ -123,10 +112,6 @@ func (server *Server) getLocation(ctx *gin.Context) {
location, err := server.Store.GetLocation(ctx, req.ID) location, err := server.Store.GetLocation(ctx, req.ID)
if err != nil { if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, ErrorResponse(err, ""))
return
}
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
return return
} }

View File

@ -1,166 +1,31 @@
package api_test package api_test
import ( import (
"bytes"
"database/sql" "database/sql"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing" "testing"
"time"
mockdb "git.nochill.in/nochill/hiling_go/db/mock"
db "git.nochill.in/nochill/hiling_go/db/sqlc" db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util" "git.nochill.in/nochill/hiling_go/util"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock" "go.uber.org/mock/gomock"
) )
func TestGetLocationsAPI(t *testing.T) { func TestGetListLocationsAPI(t *testing.T) {
location := randomLocation()
testCases := []struct {
name string
id int32
buildStubs func(store *mockdb.MockStore)
checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder)
}{
{
name: "OK",
id: location.ID,
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
GetLocation(gomock.Any(), gomock.Eq(location.ID)).
Times(1).
Return(location, nil)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockdb.NewMockStore(ctrl)
tc.buildStubs(store)
server := newTestServer(t, store)
recorder := httptest.NewRecorder()
url := fmt.Sprintf("/location/%d", tc.id)
request, err := http.NewRequest(http.MethodGet, url, nil)
require.NoError(t, err)
server.Router.ServeHTTP(recorder, request)
tc.checkResponse(t, recorder)
})
}
} }
func TestCreateLocationAPI(t *testing.T) { func TestCreateLocationAPI(t *testing.T) {
location := db.CreateLocationParams{ _ = db.CreateLocationParams{
Address: util.RandomString(10), Address: util.RandomString(10),
Name: util.RandomString(10), Name: util.RandomString(10),
SubmittedBy: 1, SubmittedBy: int32(util.RandomInt(0, 10)),
RegencyID: 1305, RegencyID: 1305,
GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(10)}, GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(10)},
} }
testCases := []struct { t.Run("OK", func(t *testing.T) {
name string ctrl := gomock.NewController(t)
body db.CreateLocationParams defer ctrl.Finish()
buildStubs func(store *mockdb.MockStore)
checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder)
}{
{
name: "OK",
body: location,
buildStubs: func(store *mockdb.MockStore) {
arg := db.CreateLocationParams{
Address: location.Address,
Name: location.Name,
SubmittedBy: location.SubmittedBy,
RegencyID: location.RegencyID,
GoogleMapsLink: location.GoogleMapsLink,
}
store.EXPECT().
CreateLocation(gomock.Any(), gomock.Eq(arg)).
Times(1).
Return(nil)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
},
},
{
name: "Bad Request",
body: db.CreateLocationParams{},
buildStubs: func(store *mockdb.MockStore) {
store.EXPECT().
CreateLocation(gomock.Any(), gomock.Any()).
Times(0)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusBadRequest, recorder.Code)
},
},
}
for i := range testCases { // store := mockdb.MockStore
tc := testCases[i] })
t.Run(tc.name, func(t *testing.T) {
bodyBuffer := new(bytes.Buffer)
mw := multipart.NewWriter(bodyBuffer)
mw.WriteField("address", tc.body.Address)
mw.WriteField("name", tc.body.Name)
mw.WriteField("submitted_by", fmt.Sprintf("%d", tc.body.SubmittedBy))
mw.WriteField("regency_id", fmt.Sprintf("%d", tc.body.RegencyID))
mw.WriteField("google_maps_link", tc.body.GoogleMapsLink.String)
mw.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
store := mockdb.NewMockStore(ctrl)
tc.buildStubs(store)
server := newTestServer(t, store)
recorder := httptest.NewRecorder()
url := "/locations"
request, err := http.NewRequest(http.MethodPost, url, bodyBuffer)
request.Header.Set("Content-Type", mw.FormDataContentType())
require.NoError(t, err)
server.Router.ServeHTTP(recorder, request)
tc.checkResponse(t, recorder)
})
}
}
func randomLocation() db.Location {
return db.Location{
ID: int32(util.RandomInt(1, 20)),
Address: util.RandomString(10),
Name: util.RandomString(10),
GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(20)},
SubmittedBy: 1,
TotalVisited: sql.NullInt32{Valid: true, Int32: int32(util.RandomInt(0, 32))},
Thumbnail: sql.NullString{Valid: false, String: ""},
RegencyID: 1305,
IsDeleted: sql.NullBool{Valid: true, Bool: false},
CreatedAt: sql.NullTime{Valid: true, Time: time.Now()},
UpdatedAt: sql.NullTime{Valid: true, Time: time.Now()},
ApprovedBy: sql.NullInt32{Valid: true, Int32: 1},
ApprovedAt: sql.NullTime{Valid: true, Time: time.Now()},
}
} }

View File

@ -65,18 +65,18 @@ func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call
} }
// GetListLocations mocks base method. // GetListLocations mocks base method.
func (m *MockStore) GetListLocations(arg0 context.Context) ([]db.Location, error) { func (m *MockStore) GetListLocations(arg0 context.Context, arg1 db.GetListLocationsParams) ([]db.Location, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetListLocations", arg0) ret := m.ctrl.Call(m, "GetListLocations", arg0, arg1)
ret0, _ := ret[0].([]db.Location) ret0, _ := ret[0].([]db.Location)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetListLocations indicates an expected call of GetListLocations. // GetListLocations indicates an expected call of GetListLocations.
func (mr *MockStoreMockRecorder) GetListLocations(arg0 interface{}) *gomock.Call { func (mr *MockStoreMockRecorder) GetListLocations(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListLocations", reflect.TypeOf((*MockStore)(nil).GetListLocations), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListLocations", reflect.TypeOf((*MockStore)(nil).GetListLocations), arg0, arg1)
} }
// GetLocation mocks base method. // GetLocation mocks base method.

View File

@ -1,5 +1,7 @@
-- name: GetListLocations :many -- name: GetListLocations :many
SELECT * FROM locations; SELECT * FROM locations
LIMIT $1
OFFSET $2;
-- name: GetLocation :one -- name: GetLocation :one
SELECT * FROM locations SELECT * FROM locations

View File

@ -43,10 +43,17 @@ func (q *Queries) CreateLocation(ctx context.Context, arg CreateLocationParams)
const getListLocations = `-- name: GetListLocations :many const getListLocations = `-- name: GetListLocations :many
SELECT id, address, name, google_maps_link, submitted_by, total_visited, thumbnail, regency_id, is_deleted, created_at, updated_at, approved_by, approved_at FROM locations SELECT id, address, name, google_maps_link, submitted_by, total_visited, thumbnail, regency_id, is_deleted, created_at, updated_at, approved_by, approved_at FROM locations
LIMIT $1
OFFSET $2
` `
func (q *Queries) GetListLocations(ctx context.Context) ([]Location, error) { type GetListLocationsParams struct {
rows, err := q.db.QueryContext(ctx, getListLocations) Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) GetListLocations(ctx context.Context, arg GetListLocationsParams) ([]Location, error) {
rows, err := q.db.QueryContext(ctx, getListLocations, arg.Limit, arg.Offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -11,7 +11,7 @@ import (
type Querier interface { type Querier interface {
CreateLocation(ctx context.Context, arg CreateLocationParams) error CreateLocation(ctx context.Context, arg CreateLocationParams) error
CreateUser(ctx context.Context, arg CreateUserParams) (User, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
GetListLocations(ctx context.Context) ([]Location, error) GetListLocations(ctx context.Context, arg GetListLocationsParams) ([]Location, error)
GetLocation(ctx context.Context, id int32) (Location, error) GetLocation(ctx context.Context, id int32) (Location, error)
UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)

View File

@ -2,8 +2,6 @@ package db
import ( import (
"database/sql" "database/sql"
"github.com/yiplee/sqlc"
) )
type Store interface { type Store interface {
@ -18,7 +16,7 @@ type SQLStore struct {
func NewStore(db *sql.DB) Store { func NewStore(db *sql.DB) Store {
return &SQLStore{ return &SQLStore{
db: db, db: db,
Queries: New(sqlc.Wrap(db)), Queries: New(db),
} }
} }

View File

@ -11,7 +11,12 @@ import (
) )
func TestGetLocationsList(t *testing.T) { func TestGetLocationsList(t *testing.T) {
locations, err := testQueries.GetListLocations(context.Background()) arg := db.GetListLocationsParams{
Limit: 10,
Offset: 0,
}
locations, err := testQueries.GetListLocations(context.Background(), arg)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, locations) require.NotEmpty(t, locations)
} }

3
go.mod
View File

@ -45,13 +45,10 @@ require (
github.com/subosito/gotenv v1.4.2 // indirect github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yiplee/nap v1.0.1 // indirect
github.com/yiplee/sqlc v1.0.2 // indirect
go.uber.org/mock v0.2.0 // indirect go.uber.org/mock v0.2.0 // indirect
golang.org/x/arch v0.5.0 // indirect golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.0 // indirect golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.13.0 // indirect golang.org/x/tools v0.13.0 // indirect

8
go.sum
View File

@ -176,7 +176,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -231,10 +230,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yiplee/nap v1.0.1 h1:5p8KAIkYy+PIMGSk+ScF13Hh/OFkIEBHPuD14OFvStg=
github.com/yiplee/nap v1.0.1/go.mod h1:7Zvro/en8ARhkqgv3vpj037yJSBRvGeNyj5Np5XUFgc=
github.com/yiplee/sqlc v1.0.2 h1:GcWRpoKb0jRPuaYhiXex/LhJWkiCWQNnUjgOMQHgvJQ=
github.com/yiplee/sqlc v1.0.2/go.mod h1:jZaVoaxbMKXkjmZAl1uOyBKf71/rhIStVcoZrg68X3c=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -354,9 +349,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

@ -1,14 +1,12 @@
version: "2" version: "1"
sql: packages:
- engine: "postgresql" - name: "db"
queries: "./db/queries" path: "./db/sqlc"
schema: "./db/migrations" queries: "./db/queries/"
gen: schema: "./db/migrations/"
go: engine: "postgresql"
package: "db" emit_json_tags: true
out: "./db/sqlc" emit_prepared_queries: false
emit_json_tags: true emit_interface: true
emit_prepared_queries: false emit_exact_table_names: false
emit_interface: true emit_empty_slices: true
emit_exact_table_names: false
emit_empty_slices: true