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" "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 {
@ -60,11 +61,21 @@ 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 {
ctx.JSON(http.StatusConflict, ErrorResponse(err, fmt.Sprintf("Something went wrong, code: %s", pqErr.Code.Name()))) 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 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
} }
@ -85,12 +96,12 @@ func (server *Server) getListLocations(ctx *gin.Context) {
return return
} }
arg := db.GetListLocationsParams{ locations, err := server.Store.GetListLocations(ysqlc.Build(ctx, func(builder *ysqlc.Builder) {
Limit: req.PageSize, builder.Limit(int(req.PageSize))
Offset: (req.Page - 1) * 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 { if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong")) ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong"))
return return
@ -112,6 +123,10 @@ 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,31 +1,166 @@
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 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) { func TestCreateLocationAPI(t *testing.T) {
_ = db.CreateLocationParams{ location := db.CreateLocationParams{
Address: util.RandomString(10), Address: util.RandomString(10),
Name: util.RandomString(10), Name: util.RandomString(10),
SubmittedBy: int32(util.RandomInt(0, 10)), SubmittedBy: 1,
RegencyID: 1305, RegencyID: 1305,
GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(10)}, GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(10)},
} }
t.Run("OK", func(t *testing.T) { 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)
},
},
}
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) ctrl := gomock.NewController(t)
defer ctrl.Finish() defer ctrl.Finish()
// store := mockdb.MockStore 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, arg1 db.GetListLocationsParams) ([]db.Location, error) { func (m *MockStore) GetListLocations(arg0 context.Context) ([]db.Location, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetListLocations", arg0, arg1) ret := m.ctrl.Call(m, "GetListLocations", arg0)
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, arg1 interface{}) *gomock.Call { func (mr *MockStoreMockRecorder) GetListLocations(arg0 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, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListLocations", reflect.TypeOf((*MockStore)(nil).GetListLocations), arg0)
} }
// GetLocation mocks base method. // GetLocation mocks base method.

View File

@ -1,7 +1,5 @@
-- 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,17 +43,10 @@ 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
` `
type GetListLocationsParams struct { func (q *Queries) GetListLocations(ctx context.Context) ([]Location, error) {
Limit int32 `json:"limit"` rows, err := q.db.QueryContext(ctx, getListLocations)
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, arg GetListLocationsParams) ([]Location, error) GetListLocations(ctx context.Context) ([]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,6 +2,8 @@ package db
import ( import (
"database/sql" "database/sql"
"github.com/yiplee/sqlc"
) )
type Store interface { type Store interface {
@ -16,7 +18,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(db), Queries: New(sqlc.Wrap(db)),
} }
} }

View File

@ -11,12 +11,7 @@ import (
) )
func TestGetLocationsList(t *testing.T) { func TestGetLocationsList(t *testing.T) {
arg := db.GetListLocationsParams{ locations, err := testQueries.GetListLocations(context.Background())
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,10 +45,13 @@ 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,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/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=
@ -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/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=
@ -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-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,10 +1,12 @@
version: "1" version: "2"
packages: sql:
- name: "db" - engine: "postgresql"
path: "./db/sqlc" queries: "./db/queries"
queries: "./db/queries/" schema: "./db/migrations"
schema: "./db/migrations/" gen:
engine: "postgresql" go:
package: "db"
out: "./db/sqlc"
emit_json_tags: true emit_json_tags: true
emit_prepared_queries: false emit_prepared_queries: false
emit_interface: true emit_interface: true