Compare commits

..

3 Commits

Author SHA1 Message Date
a130a826bc fix import conflict 2023-09-14 14:58:16 +07:00
8e132dbdcd update sqlc ver 2 2023-09-14 14:47:45 +07:00
1a6ea59e92 adjust query for dynamic arg 2023-09-14 13:49:03 +07:00
11 changed files with 202 additions and 51 deletions

View File

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

View File

@ -1,31 +1,166 @@
package api_test
import (
"bytes"
"database/sql"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"time"
mockdb "git.nochill.in/nochill/hiling_go/db/mock"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"git.nochill.in/nochill/hiling_go/util"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestGetListLocationsAPI(t *testing.T) {
func TestGetLocationsAPI(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) {
_ = db.CreateLocationParams{
location := db.CreateLocationParams{
Address: util.RandomString(10),
Name: util.RandomString(10),
SubmittedBy: int32(util.RandomInt(0, 10)),
SubmittedBy: 1,
RegencyID: 1305,
GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(10)},
}
t.Run("OK", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
testCases := []struct {
name string
body db.CreateLocationParams
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)
},
},
}
// store := mockdb.MockStore
})
for i := range testCases {
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.
func (m *MockStore) GetListLocations(arg0 context.Context, arg1 db.GetListLocationsParams) ([]db.Location, error) {
func (m *MockStore) GetListLocations(arg0 context.Context) ([]db.Location, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetListLocations", arg0, arg1)
ret := m.ctrl.Call(m, "GetListLocations", arg0)
ret0, _ := ret[0].([]db.Location)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetListLocations indicates an expected call of GetListLocations.
func (mr *MockStoreMockRecorder) GetListLocations(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStoreMockRecorder) GetListLocations(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListLocations", reflect.TypeOf((*MockStore)(nil).GetListLocations), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListLocations", reflect.TypeOf((*MockStore)(nil).GetListLocations), arg0)
}
// GetLocation mocks base method.

View File

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

View File

@ -43,17 +43,10 @@ func (q *Queries) CreateLocation(ctx context.Context, arg CreateLocationParams)
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
LIMIT $1
OFFSET $2
`
type GetListLocationsParams struct {
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)
func (q *Queries) GetListLocations(ctx context.Context) ([]Location, error) {
rows, err := q.db.QueryContext(ctx, getListLocations)
if err != nil {
return nil, err
}

View File

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

View File

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

View File

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

3
go.mod
View File

@ -45,10 +45,13 @@ require (
github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // 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
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.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/text v0.13.0 // indirect
golang.org/x/tools v0.13.0 // indirect

8
go.sum
View File

@ -176,6 +176,7 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
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/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/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -230,6 +231,10 @@ 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/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -349,6 +354,9 @@ 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-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-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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

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