Compare commits

...

14 Commits

Author SHA1 Message Date
2f05a2f8e7 add news events 2023-10-11 16:31:52 +07:00
f8ecd20220 add update users and refactor 2023-10-09 16:25:39 +07:00
eb9774cce5 migration for users table 2023-10-09 16:25:08 +07:00
b31bc76a24 add user profile stats 2023-10-04 19:51:29 +07:00
a2d4dfb282 update todo 2023-10-03 19:44:40 +07:00
8dbebd9f17 updaet add location into trasnaction 2023-10-03 19:44:31 +07:00
641831a3ae add follow flow 2023-10-03 16:14:58 +07:00
5a5d157882 adjust with multiple image locations 2023-10-03 14:56:57 +07:00
110cbfeec0 fix schema and csv imports 2023-10-03 14:56:08 +07:00
47e692bf30 add new TODO 2023-10-03 14:55:42 +07:00
6cd35d7ffa add region, regency, province 2023-10-02 08:49:41 +07:00
ddca119e45 update location data type not null 2023-10-02 08:49:19 +07:00
198e311407 add location type 2023-09-29 14:45:25 +07:00
1f055e92d4 add error message user not found 2023-09-27 22:34:52 +07:00
47 changed files with 1434 additions and 242 deletions

8
TODO
View File

@ -1,3 +1,9 @@
- COMMENTS FLOW
- USER PROFILE FLOW - USER PROFILE FLOW
- ADMIN FLOW (SUBMISSION, USER REPORTS)
- BEST LOCATIONS FILTER ( MIN REVIEWS AND TAGS) - BEST LOCATIONS FILTER ( MIN REVIEWS AND TAGS)
- PAGINATION
- OPEN GRAPH PARSER
- USERS FEEED
## IF THERES TIME LEFT
- STORIES FLOW

6
api/api.go Normal file
View File

@ -0,0 +1,6 @@
package api
type BaseGetListRequest struct {
Page int32 `form:"page" binding:"required,min=1"`
PageSize int32 `form:"page_size" binding:"required,min=5"`
}

View File

@ -11,7 +11,6 @@ import (
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/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lib/pq"
ysqlc "github.com/yiplee/sqlc" ysqlc "github.com/yiplee/sqlc"
) )
@ -20,27 +19,30 @@ type createLocationReq struct {
Name string `form:"name" binding:"required"` Name string `form:"name" binding:"required"`
SubmittedBy int32 `form:"submitted_by" binding:"required,number"` SubmittedBy int32 `form:"submitted_by" binding:"required,number"`
RegencyID int16 `form:"regency_id" binding:"required,number"` RegencyID int16 `form:"regency_id" binding:"required,number"`
LocationType string `form:"location_type" binding:"required"`
GoogleMapsLink string `form:"google_maps_link"` GoogleMapsLink string `form:"google_maps_link"`
} }
func (server *Server) createLocation(ctx *gin.Context) { func (server *Server) createLocation(ctx *gin.Context) {
var req createLocationReq var req createLocationReq
var imgPath string var imgPath string
var tempImg []db.CreateImageParams
var thumbnail, _ = ctx.FormFile("thumbnail")
if err := ctx.Bind(&req); err != nil { if err := ctx.Bind(&req); err != nil {
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err)) ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
return return
} }
if thumbnail != nil { form, _ := ctx.MultipartForm()
img := thumbnail thumbnails := form.File["thumbnail"]
if len(thumbnails) > 0 {
for _, img := range thumbnails {
fileExt := filepath.Ext(img.Filename) fileExt := filepath.Ext(img.Filename)
now := time.Now() now := time.Now()
dir := fmt.Sprintf("public/upload/images/locations/%s/thumbnail", req.Name) dir := fmt.Sprintf("public/upload/images/locations/%s/thumbnail", req.Name)
osFilename := fmt.Sprintf("%s%s%s", util.RandomString(5), fmt.Sprintf("%v", now.Unix()), fileExt) osFilename := fmt.Sprintf("%s%s%s", util.RandomString(5), fmt.Sprintf("%v", now.Unix()), fileExt)
imgPath = fmt.Sprintf("%s%s", dir, osFilename) imgPath = fmt.Sprintf("%s/%s", dir, osFilename)
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
os.Mkdir(dir, 0775) os.Mkdir(dir, 0775)
@ -52,32 +54,25 @@ func (server *Server) createLocation(ctx *gin.Context) {
} }
} }
arg := db.CreateLocationParams{ arg := db.CreateLocationTxParams{
Address: req.Address, Address: req.Address,
Name: req.Name, Name: req.Name,
LocationType: db.LocationType(req.LocationType),
SubmittedBy: req.SubmittedBy, SubmittedBy: req.SubmittedBy,
RegencyID: req.RegencyID, RegencyID: req.RegencyID,
IsDeleted: false,
ApprovedBy: sql.NullInt32{Int32: 0, Valid: false},
GoogleMapsLink: sql.NullString{Valid: len(req.GoogleMapsLink) > 0, String: req.GoogleMapsLink}, GoogleMapsLink: sql.NullString{Valid: len(req.GoogleMapsLink) > 0, String: req.GoogleMapsLink},
Thumbnail: tempImg,
} }
err := server.Store.CreateLocation(ctx, arg) err := server.Store.CreateLocationTx(ctx, arg)
if err != nil { if err != nil {
if pqErr, ok := err.(*pq.Error); ok { ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to save Location"))
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"))
return
} }
ctx.Writer.WriteHeader(http.StatusOK) ctx.Writer.WriteHeader(http.StatusOK)

71
api/news_event.go Normal file
View File

@ -0,0 +1,71 @@
package api
import (
"database/sql"
"net/http"
db "git.nochill.in/nochill/hiling_go/db/sqlc"
"github.com/gin-gonic/gin"
)
type CreateNewsEventsReq struct {
Url string `json:"url" binding:"required,url"`
Title string `json:"title" binding:"required"`
Source string `json:"source"`
Description string `json:"description"`
SubmittedBy int32 `json:"submitted_by" binding:"required,numeric"`
}
func (server *Server) createNews(ctx *gin.Context) {
var req CreateNewsEventsReq
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
return
}
err := server.Store.CreateNewsEvents(ctx, db.CreateNewsEventsParams{
Title: req.Title,
Url: req.Url,
Source: req.Source,
Description: sql.NullString{Valid: len(req.Description) > 0, String: req.Description},
SubmittedBy: req.SubmittedBy,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to save news/evnts"))
return
}
ctx.Writer.WriteHeader(http.StatusCreated)
}
type GetNewsEventsListReq struct {
BaseGetListRequest
Approved int8 `form:"is_with_approval"`
}
func (server *Server) GetNewsEventsList(ctx *gin.Context) {
var req GetNewsEventsListReq
isWithApproval := ""
if err := ctx.ShouldBindQuery(&req); err != nil {
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
return
}
if req.Approved > 0 {
isWithApproval = "WHERE approved_by IS NOT NULL"
}
news, err := server.Store.GetNewsEventsList(ctx, db.GetNewsEventsListParams{
Limit: req.PageSize,
Offset: (req.Page - 1) * req.PageSize,
IsWithApproved: isWithApproval,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to get news / events"))
return
}
ctx.JSON(http.StatusOK, news)
}

37
api/region.go Normal file
View File

@ -0,0 +1,37 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
)
func (server *Server) getListRegions(ctx *gin.Context) {
regions, err := server.Store.GetListRegions(ctx)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to get regions"))
return
}
ctx.JSON(http.StatusOK, regions)
}
func (server *Server) getListProvinces(ctx *gin.Context) {
provinces, err := server.Store.GetListProvinces(ctx)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to get regions"))
return
}
ctx.JSON(http.StatusOK, provinces)
}
func (server *Server) getListRegencies(ctx *gin.Context) {
regencies, err := server.Store.GetListRegencies(ctx)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to get regions"))
return
}
ctx.JSON(http.StatusOK, regencies)
}

View File

@ -39,15 +39,16 @@ func (server *Server) getRoutes() {
router.Use(cors.New(cors.Config{ router.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:5173"}, AllowOrigins: []string{"http://localhost:5173"},
AllowCredentials: true, AllowCredentials: true,
AllowHeaders: []string{"Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "accept", "origin", "Cache-Control", "X-Requested-With"}, AllowHeaders: []string{"Content-Type", "Content-Length", "Accept-Encoding", "Authorization", "accept", "origin", "Cache-Control", "X-Requested-With", "X-XSRF-TOKEN"},
AllowMethods: []string{"POST", "PUT", "GET", "DELETE", "PATCH"}, AllowMethods: []string{"POST", "PUT", "GET", "DELETE", "PATCH"},
})) }))
// router.Use(CORSMiddleware())
router.POST("/user/signup", server.createUser) router.POST("/user/signup", server.createUser)
router.POST("/user/login", server.login) router.POST("/user/login", server.login)
router.POST("/user/logout", server.logout) router.POST("/user/logout", server.logout)
router.GET("/regions", server.getListRegions)
router.GET("/region/provinces", server.getListProvinces)
router.GET("/region/regencies", server.getListRegencies)
// LOCATION // LOCATION
router.POST("/locations", server.createLocation) router.POST("/locations", server.createLocation)
@ -61,10 +62,18 @@ func (server *Server) getRoutes() {
//IMAGES //IMAGES
router.GET("/images/location", server.getAllImagesByLocation) router.GET("/images/location", server.getAllImagesByLocation)
// NEWS / EVENTS
router.GET("/news-events", server.GetNewsEventsList)
// REQUIRE AUTH TOKEN // REQUIRE AUTH TOKEN
authRoutes := router.Use(authMiddleware(server.TokenMaker)) authRoutes := router.Use(authMiddleware(server.TokenMaker))
authRoutes.POST("/review/location", server.createReview) authRoutes.POST("/review/location", server.createReview)
authRoutes.PATCH("/user", server.updateUser)
authRoutes.GET("/user/review/location/:location_id", server.getUserReviewByLocation) authRoutes.GET("/user/review/location/:location_id", server.getUserReviewByLocation)
authRoutes.GET("/user/profile", server.getUserStats)
authRoutes.PATCH("/user/avatar", server.updateUserAvatar)
authRoutes.DELETE("/user/avatar", server.removeAvatar)
authRoutes.POST("/news-events", server.createNews)
server.Router = router server.Router = router
} }

View File

@ -157,7 +157,7 @@ func randomLocation() db.Location {
TotalVisited: sql.NullInt32{Valid: true, Int32: int32(util.RandomInt(0, 32))}, TotalVisited: sql.NullInt32{Valid: true, Int32: int32(util.RandomInt(0, 32))},
Thumbnail: sql.NullString{Valid: false, String: ""}, Thumbnail: sql.NullString{Valid: false, String: ""},
RegencyID: 1305, RegencyID: 1305,
IsDeleted: sql.NullBool{Valid: true, Bool: false}, IsDeleted: false,
CreatedAt: sql.NullTime{Valid: true, Time: time.Now()}, CreatedAt: sql.NullTime{Valid: true, Time: time.Now()},
UpdatedAt: sql.NullTime{Valid: true, Time: time.Now()}, UpdatedAt: sql.NullTime{Valid: true, Time: time.Now()},
ApprovedBy: sql.NullInt32{Valid: true, Int32: 1}, ApprovedBy: sql.NullInt32{Valid: true, Int32: 1},

View File

@ -2,13 +2,19 @@ package api
import ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt"
"net/http" "net/http"
"os"
"path/filepath"
"time" "time"
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"
"git.nochill.in/nochill/hiling_go/util/token"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/sqlc-dev/pqtype"
) )
type createUserRequest struct { type createUserRequest struct {
@ -124,6 +130,134 @@ func (server *Server) createUser(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res) ctx.JSON(http.StatusOK, res)
} }
func (server *Server) getUserStats(ctx *gin.Context) {
var scoreDistribution []map[string]int8
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
userStats, err := server.Store.GetUserStats(ctx, int32(authPayload.UserID))
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to get user stats"))
return
}
err = json.Unmarshal(userStats.ScoresDistribution, &scoreDistribution)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong whilet try to parse score distribution"))
return
}
var userReviews []map[string]any
if userStats.Reviews != nil {
err = json.Unmarshal(userStats.Reviews, &userReviews)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something wrong while try to parse user reviwes stats"))
return
}
} else {
userReviews = make([]map[string]any, 0)
}
ctx.JSON(http.StatusOK, gin.H{
"reviews": userReviews,
"scores_distribution": scoreDistribution,
"user_stats": userStats,
})
}
type UpdateUserRequest struct {
About string `json:"about"`
Website string `json:"website"`
SocialMedia []map[string]string `json:"social_media"`
}
func (server *Server) updateUser(ctx *gin.Context) {
var req UpdateUserRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
return
}
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
fmt.Println(req.About)
social, _ := json.Marshal(req.SocialMedia)
socialArr := pqtype.NullRawMessage{
RawMessage: social,
Valid: len(req.SocialMedia) > 0,
}
user, err := server.Store.UpdateUser(ctx, db.UpdateUserParams{
About: sql.NullString{String: req.About, Valid: true},
SocialMedia: socialArr,
Website: sql.NullString{String: req.Website, Valid: len(req.Website) > 0},
ID: int32(authPayload.UserID),
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to update user"))
return
}
ctx.JSON(http.StatusOK, user)
}
func (server *Server) updateUserAvatar(ctx *gin.Context) {
file, err := ctx.FormFile("file")
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to parse file"))
return
}
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
fileExt := filepath.Ext(file.Filename)
now := time.Now()
dir := fmt.Sprintf("public/upload/images/user/%d/avatar", authPayload.UserID)
osFilename := fmt.Sprintf("%s%s%s", util.RandomString(5), fmt.Sprintf("%v", now.Unix()), fileExt)
imgPath := fmt.Sprintf("%s/%s", dir, osFilename)
if _, err := os.Stat(dir); os.IsNotExist(err) {
os.Mkdir(dir, 0775)
}
if err := ctx.SaveUploadedFile(file, imgPath); err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Error while try to save thumbnail image"))
return
}
url, err := server.Store.UpdateAvatar(ctx, db.UpdateAvatarParams{
ID: int32(authPayload.UserID),
AvatarPicture: sql.NullString{Valid: true, String: imgPath},
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to save image to database"))
return
}
ctx.JSON(http.StatusOK, gin.H{"image_url": url.String})
}
func (server *Server) removeAvatar(ctx *gin.Context) {
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
_, err := server.Store.UpdateAvatar(ctx, db.UpdateAvatarParams{
AvatarPicture: sql.NullString{String: "", Valid: false},
ID: int32(authPayload.UserID),
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to update user avatar"))
return
}
ctx.Writer.WriteHeader(http.StatusNoContent)
}
func (server *Server) login(ctx *gin.Context) { func (server *Server) login(ctx *gin.Context) {
var req createUserRequest var req createUserRequest
@ -135,7 +269,7 @@ func (server *Server) login(ctx *gin.Context) {
user, err := server.Store.GetUser(ctx, req.Username) user, err := server.Store.GetUser(ctx, req.Username)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, ErrorResponse(err, "")) ctx.JSON(http.StatusNotFound, ErrorResponse(err, "User not found"))
return return
} }
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong whlie try to get user")) ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong whlie try to get user"))

View File

@ -1,13 +1,13 @@
id,address,name,submitted_by,thumbnail,regency_id,google_maps_link,approved_by id,location_type,address,name,submitted_by,thumbnail,regency_id,google_maps_link,approved_by,is_deleted
1#Jalan Raya Beside the bridge Ubud#Murnis Warung#1#https://cdn.discordapp.com/attachments/743422487882104837/1150972798320267304/image.png#5104#https://www.google.com/maps/place/Murni's+Warung/@-8.5048696,115.2553417,19z/data=!4m6!3m5!1s0x2dd23d3e0ffaa071:0xf2fa69b4cb211e41!8m2!3d-8.5051184!4d115.2547196!16s%2Fg%2F1tdsmcq7?entry=ttu#1 1#culinary#Jalan Raya Beside the bridge Ubud#Murnis Warung#1#https://cdn.discordapp.com/attachments/743422487882104837/1150972798320267304/image.png#5104#https://www.google.com/maps/place/Murni's+Warung/@-8.5048696,115.2553417,19z/data=!4m6!3m5!1s0x2dd23d3e0ffaa071:0xf2fa69b4cb211e41!8m2!3d-8.5051184!4d115.2547196!16s%2Fg%2F1tdsmcq7?entry=ttu#1#false
2#Jl.Taman Wijaya Kusuma Ps. Baru Kecamatan Sawah Besar#Masjid Istiqlal#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/04/11/31/33/istiqlal-mosque-mesjid.jpg?w=500&h=-1&s=1#3173#https://www.google.com/maps/place/Masjid+Istiqlal/@-6.1703155,106.8308434,19z/data=!4m15!1m7!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!2sMasjid+Istiqlal!8m2!3d-6.17017!4d106.83139!16zL20vMDRzam1q!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!8m2!3d-6.17017!4d106.83139!15sCg9tYXNqaWQgaXN0aXFsYWxaESIPbWFzamlkIGlzdGlxbGFskgEGbW9zcXVl4AEA!16zL20vMDRzam1q?entry=ttu#1 2#other#Jl.Taman Wijaya Kusuma Ps. Baru Kecamatan Sawah Besar#Masjid Istiqlal#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/04/11/31/33/istiqlal-mosque-mesjid.jpg?w=500&h=-1&s=1#3173#https://www.google.com/maps/place/Masjid+Istiqlal/@-6.1703155,106.8308434,19z/data=!4m15!1m7!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!2sMasjid+Istiqlal!8m2!3d-6.17017!4d106.83139!16zL20vMDRzam1q!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!8m2!3d-6.17017!4d106.83139!15sCg9tYXNqaWQgaXN0aXFsYWxaESIPbWFzamlkIGlzdGlxbGFskgEGbW9zcXVl4AEA!16zL20vMDRzam1q?entry=ttu#1#false
3#Jl. Mayjend Sungkono no. 89#Hotel Ciputra World Surabaya#1#https://lh5.googleusercontent.com/p/AF1QipOvHDO-M6riRoqBrWU3MskhwL_bue8JmN9faq7Q=w500-h500-k-no#3578#https://www.google.com/maps/place/Ciputra+World+Hotel+Surabaya/@-7.2923061,112.7191552,15z/data=!4m2!3m1!1s0x0:0x736a9c49dcc2ac42?sa=X&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgVEAA&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgjEAc#1 3#other#Jl. Mayjend Sungkono no. 89#Hotel Ciputra World Surabaya#1#https://lh5.googleusercontent.com/p/AF1QipOvHDO-M6riRoqBrWU3MskhwL_bue8JmN9faq7Q=w500-h500-k-no#3578#https://www.google.com/maps/place/Ciputra+World+Hotel+Surabaya/@-7.2923061,112.7191552,15z/data=!4m2!3m1!1s0x0:0x736a9c49dcc2ac42?sa=X&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgVEAA&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgjEAc#1#false
4#Jl. Taman Safari No.101 . B Cibeureum Kec. Cisarua#Club Huis#1#https://media-cdn.tripadvisor.com/media/photo-o/0d/6a/5d/63/our-peaceful-backyard.jpg#3201#https://www.google.com/maps/place/Club+Huis/@-6.7027857,106.9453741,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69b679d7a09e01:0xf9fc2df396f09977!8m2!3d-6.7027857!4d106.947949!16s%2Fg%2F11c57lh8ky?entry=ttu#1 4#amusement park#Jl. Taman Safari No.101 . B Cibeureum Kec. Cisarua#Club Huis#1#https://media-cdn.tripadvisor.com/media/photo-o/0d/6a/5d/63/our-peaceful-backyard.jpg#3201#https://www.google.com/maps/place/Club+Huis/@-6.7027857,106.9453741,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69b679d7a09e01:0xf9fc2df396f09977!8m2!3d-6.7027857!4d106.947949!16s%2Fg%2F11c57lh8ky?entry=ttu#1#false
5#Desa Tambakrejo Kecamatan Sumbermanjing Wetan#Pulau Sempu#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/11/3b/06/a5/pulau-sempu.jpg?w=500&h=-1&s=1#3507#https://www.google.com/maps/place/Pulau+Sempu/@-8.446621,112.6746143,14z/data=!3m1!4b1!4m6!3m5!1s0x2dd60120edbc901f:0x8efd89687a308993!8m2!3d-8.4428564!4d112.6973355!16s%2Fm%2F0r8k540?entry=ttu#1 5#other#Desa Tambakrejo Kecamatan Sumbermanjing Wetan#Pulau Sempu#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/11/3b/06/a5/pulau-sempu.jpg?w=500&h=-1&s=1#3507#https://www.google.com/maps/place/Pulau+Sempu/@-8.446621,112.6746143,14z/data=!3m1!4b1!4m6!3m5!1s0x2dd60120edbc901f:0x8efd89687a308993!8m2!3d-8.4428564!4d112.6973355!16s%2Fm%2F0r8k540?entry=ttu#1#false
6#Jl. Bukit Golf I BSD Sektor VI Lengkong Karya Kec. Serpong Utara#Damai Indah Golf#1#https://lh3.googleusercontent.com/p/AF1QipN5Z-0J6vIfIO6gqPO0z5HDWlNKqp0t816XIJPS=s680-w500-h500#3674#https://www.google.com/maps/place/Damai+Indah+Golf+-+BSD+Course/@-6.2815644,106.6496566,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69fb152983d973:0x89e58e219f8b93ef!8m2!3d-6.2815644!4d106.6522315!16s%2Fg%2F11c54c9r94?entry=ttu#1 6#other#Jl. Bukit Golf I BSD Sektor VI Lengkong Karya Kec. Serpong Utara#Damai Indah Golf#1#https://lh3.googleusercontent.com/p/AF1QipN5Z-0J6vIfIO6gqPO0z5HDWlNKqp0t816XIJPS=s680-w500-h500#3674#https://www.google.com/maps/place/Damai+Indah+Golf+-+BSD+Course/@-6.2815644,106.6496566,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69fb152983d973:0x89e58e219f8b93ef!8m2!3d-6.2815644!4d106.6522315!16s%2Fg%2F11c54c9r94?entry=ttu#1#false
7#Jl. P. Mangkubumi No.72A Cokrodiningratan Kec. Jetis#Hotel Tentrem Yogyakarta#1#https://cdn.discordapp.com/attachments/743422487882104837/1150987888553623653/image.png#3471#https://www.google.com/maps?q=Hotel+Tentrem+Yogyakarta&source=lmns&entry=mc&bih=1115&biw=2124&hl=en-US&sa=X&ved=2ahUKEwjjl-HHiKSBAxUu5jgGHTU3BiwQ0pQJKAJ6BAgBEAY#1 7#other#Jl. P. Mangkubumi No.72A Cokrodiningratan Kec. Jetis#Hotel Tentrem Yogyakarta#1#https://cdn.discordapp.com/attachments/743422487882104837/1150987888553623653/image.png#3471#https://www.google.com/maps?q=Hotel+Tentrem+Yogyakarta&source=lmns&entry=mc&bih=1115&biw=2124&hl=en-US&sa=X&ved=2ahUKEwjjl-HHiKSBAxUu5jgGHTU3BiwQ0pQJKAJ6BAgBEAY#1#false
8#Moluo Kec.Kwandang#Pulau Saronde Gorontalo#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0d/ec/58/21/saronde-island-a-place.jpg?w=700&h=-1&s=1#7505#https://www.google.com/maps/place/Pulau+Saronde+Gorontalo/@0.9263376,122.8613201,17z/data=!3m1!4b1!4m6!3m5!1s0x32795bf34dff4467:0xa8beb2a832ae8176!8m2!3d0.9263376!4d122.863895!16s%2Fg%2F11l241cc1d?hl=id&entry=ttu#1 8#other#Moluo Kec.Kwandang#Pulau Saronde Gorontalo#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0d/ec/58/21/saronde-island-a-place.jpg?w=700&h=-1&s=1#7505#https://www.google.com/maps/place/Pulau+Saronde+Gorontalo/@0.9263376,122.8613201,17z/data=!3m1!4b1!4m6!3m5!1s0x32795bf34dff4467:0xa8beb2a832ae8176!8m2!3d0.9263376!4d122.863895!16s%2Fg%2F11l241cc1d?hl=id&entry=ttu#1#false
9#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu#1 9#beach#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu#1#false
10#Pulau Padar#Pulau Padar#2#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/18/4e/11/f3/padar-island.jpg?w=800&h=-1&s=1#5315#https://www.google.com/maps/place/Pulau+Padar/@-8.6554183,119.570686,14.83z/data=!4m6!3m5!1s0x2db4f84ff6cd01ab:0xf7e6fd33b692a898!8m2!3d-8.6489909!4d119.5832593!16s%2Fg%2F121d16wd?entry=ttu#1 10#other#Pulau Padar#Pulau Padar#2#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/18/4e/11/f3/padar-island.jpg?w=800&h=-1&s=1#5315#https://www.google.com/maps/place/Pulau+Padar/@-8.6554183,119.570686,14.83z/data=!4m6!3m5!1s0x2db4f84ff6cd01ab:0xf7e6fd33b692a898!8m2!3d-8.6489909!4d119.5832593!16s%2Fg%2F121d16wd?entry=ttu#1#false
11#Jl. Ahmad Yani 18, Kel. Kelimutu, Kec. Ende Tengah#Sari rasa#3#https://cdn.discordapp.com/attachments/743422487882104837/1151903310295601172/image.png#5315#https://www.google.com/maps/place/Rumah+Makan+Cha+Cha/@-8.6153697,120.4652533,21z/data=!4m6!3m5!1s0x2db37469e54c0dd3:0x36ac988c726ed544!8m2!3d-8.6153631!4d120.465275!16s%2Fg%2F11bwpclp91?entry=ttu#1 11#culinary#Jl. Ahmad Yani 18, Kel. Kelimutu, Kec. Ende Tengah#Sari rasa#3#https://cdn.discordapp.com/attachments/743422487882104837/1151903310295601172/image.png#5315#https://www.google.com/maps/place/Rumah+Makan+Cha+Cha/@-8.6153697,120.4652533,21z/data=!4m6!3m5!1s0x2db37469e54c0dd3:0x36ac988c726ed544!8m2!3d-8.6153631!4d120.465275!16s%2Fg%2F11bwpclp91?entry=ttu#1#false
12#Danau Sentani#Danau Sentani#3#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/c7/e8/17/20190630-143008-largejpg.jpg?w=800&h=-1&s=1#9403#https://www.google.com/maps/place/Danau+Sentani/@-2.639976,140.3889748,12.46z/data=!4m7!3m6!1s0x686cf33c72660fbf:0x7e5c1e7d20d930d7!4b1!8m2!3d-2.6133004!4d140.518734!16s%2Fm%2F0tkjf6v?entry=ttu#1 12#other#Danau Sentani#Danau Sentani#3#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/c7/e8/17/20190630-143008-largejpg.jpg?w=800&h=-1&s=1#9403#https://www.google.com/maps/place/Danau+Sentani/@-2.639976,140.3889748,12.46z/data=!4m7!3m6!1s0x686cf33c72660fbf:0x7e5c1e7d20d930d7!4b1!8m2!3d-2.6133004!4d140.518734!16s%2Fm%2F0tkjf6v?entry=ttu#1#false
1 id,address,name,submitted_by,thumbnail,regency_id,google_maps_link,approved_by id,location_type,address,name,submitted_by,thumbnail,regency_id,google_maps_link,approved_by,is_deleted
2 1#Jalan Raya Beside the bridge Ubud#Murni’s Warung#1#https://cdn.discordapp.com/attachments/743422487882104837/1150972798320267304/image.png#5104#https://www.google.com/maps/place/Murni's+Warung/@-8.5048696,115.2553417,19z/data=!4m6!3m5!1s0x2dd23d3e0ffaa071:0xf2fa69b4cb211e41!8m2!3d-8.5051184!4d115.2547196!16s%2Fg%2F1tdsmcq7?entry=ttu#1 1#culinary#Jalan Raya Beside the bridge Ubud#Murni’s Warung#1#https://cdn.discordapp.com/attachments/743422487882104837/1150972798320267304/image.png#5104#https://www.google.com/maps/place/Murni's+Warung/@-8.5048696,115.2553417,19z/data=!4m6!3m5!1s0x2dd23d3e0ffaa071:0xf2fa69b4cb211e41!8m2!3d-8.5051184!4d115.2547196!16s%2Fg%2F1tdsmcq7?entry=ttu#1#false
3 2#Jl.Taman Wijaya Kusuma Ps. Baru Kecamatan Sawah Besar#Masjid Istiqlal#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/04/11/31/33/istiqlal-mosque-mesjid.jpg?w=500&h=-1&s=1#3173#https://www.google.com/maps/place/Masjid+Istiqlal/@-6.1703155,106.8308434,19z/data=!4m15!1m7!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!2sMasjid+Istiqlal!8m2!3d-6.17017!4d106.83139!16zL20vMDRzam1q!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!8m2!3d-6.17017!4d106.83139!15sCg9tYXNqaWQgaXN0aXFsYWxaESIPbWFzamlkIGlzdGlxbGFskgEGbW9zcXVl4AEA!16zL20vMDRzam1q?entry=ttu#1 2#other#Jl.Taman Wijaya Kusuma Ps. Baru Kecamatan Sawah Besar#Masjid Istiqlal#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/04/11/31/33/istiqlal-mosque-mesjid.jpg?w=500&h=-1&s=1#3173#https://www.google.com/maps/place/Masjid+Istiqlal/@-6.1703155,106.8308434,19z/data=!4m15!1m7!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!2sMasjid+Istiqlal!8m2!3d-6.17017!4d106.83139!16zL20vMDRzam1q!3m6!1s0x2e69f5ce68b5e01d:0xcafaf042d5840c6c!8m2!3d-6.17017!4d106.83139!15sCg9tYXNqaWQgaXN0aXFsYWxaESIPbWFzamlkIGlzdGlxbGFskgEGbW9zcXVl4AEA!16zL20vMDRzam1q?entry=ttu#1#false
4 3#Jl. Mayjend Sungkono no. 89#Hotel Ciputra World Surabaya#1#https://lh5.googleusercontent.com/p/AF1QipOvHDO-M6riRoqBrWU3MskhwL_bue8JmN9faq7Q=w500-h500-k-no#3578#https://www.google.com/maps/place/Ciputra+World+Hotel+Surabaya/@-7.2923061,112.7191552,15z/data=!4m2!3m1!1s0x0:0x736a9c49dcc2ac42?sa=X&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgVEAA&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgjEAc#1 3#other#Jl. Mayjend Sungkono no. 89#Hotel Ciputra World Surabaya#1#https://lh5.googleusercontent.com/p/AF1QipOvHDO-M6riRoqBrWU3MskhwL_bue8JmN9faq7Q=w500-h500-k-no#3578#https://www.google.com/maps/place/Ciputra+World+Hotel+Surabaya/@-7.2923061,112.7191552,15z/data=!4m2!3m1!1s0x0:0x736a9c49dcc2ac42?sa=X&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgVEAA&ved=2ahUKEwjJlbf8gqSBAxWtzzgGHUIkBFYQ_BJ6BAgjEAc#1#false
5 4#Jl. Taman Safari No.101 . B Cibeureum Kec. Cisarua#Club Huis#1#https://media-cdn.tripadvisor.com/media/photo-o/0d/6a/5d/63/our-peaceful-backyard.jpg#3201#https://www.google.com/maps/place/Club+Huis/@-6.7027857,106.9453741,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69b679d7a09e01:0xf9fc2df396f09977!8m2!3d-6.7027857!4d106.947949!16s%2Fg%2F11c57lh8ky?entry=ttu#1 4#amusement park#Jl. Taman Safari No.101 . B Cibeureum Kec. Cisarua#Club Huis#1#https://media-cdn.tripadvisor.com/media/photo-o/0d/6a/5d/63/our-peaceful-backyard.jpg#3201#https://www.google.com/maps/place/Club+Huis/@-6.7027857,106.9453741,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69b679d7a09e01:0xf9fc2df396f09977!8m2!3d-6.7027857!4d106.947949!16s%2Fg%2F11c57lh8ky?entry=ttu#1#false
6 5#Desa Tambakrejo Kecamatan Sumbermanjing Wetan#Pulau Sempu#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/11/3b/06/a5/pulau-sempu.jpg?w=500&h=-1&s=1#3507#https://www.google.com/maps/place/Pulau+Sempu/@-8.446621,112.6746143,14z/data=!3m1!4b1!4m6!3m5!1s0x2dd60120edbc901f:0x8efd89687a308993!8m2!3d-8.4428564!4d112.6973355!16s%2Fm%2F0r8k540?entry=ttu#1 5#other#Desa Tambakrejo Kecamatan Sumbermanjing Wetan#Pulau Sempu#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/11/3b/06/a5/pulau-sempu.jpg?w=500&h=-1&s=1#3507#https://www.google.com/maps/place/Pulau+Sempu/@-8.446621,112.6746143,14z/data=!3m1!4b1!4m6!3m5!1s0x2dd60120edbc901f:0x8efd89687a308993!8m2!3d-8.4428564!4d112.6973355!16s%2Fm%2F0r8k540?entry=ttu#1#false
7 6#Jl. Bukit Golf I BSD Sektor VI Lengkong Karya Kec. Serpong Utara#Damai Indah Golf#1#https://lh3.googleusercontent.com/p/AF1QipN5Z-0J6vIfIO6gqPO0z5HDWlNKqp0t816XIJPS=s680-w500-h500#3674#https://www.google.com/maps/place/Damai+Indah+Golf+-+BSD+Course/@-6.2815644,106.6496566,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69fb152983d973:0x89e58e219f8b93ef!8m2!3d-6.2815644!4d106.6522315!16s%2Fg%2F11c54c9r94?entry=ttu#1 6#other#Jl. Bukit Golf I BSD Sektor VI Lengkong Karya Kec. Serpong Utara#Damai Indah Golf#1#https://lh3.googleusercontent.com/p/AF1QipN5Z-0J6vIfIO6gqPO0z5HDWlNKqp0t816XIJPS=s680-w500-h500#3674#https://www.google.com/maps/place/Damai+Indah+Golf+-+BSD+Course/@-6.2815644,106.6496566,17z/data=!3m1!4b1!4m6!3m5!1s0x2e69fb152983d973:0x89e58e219f8b93ef!8m2!3d-6.2815644!4d106.6522315!16s%2Fg%2F11c54c9r94?entry=ttu#1#false
8 7#Jl. P. Mangkubumi No.72A Cokrodiningratan Kec. Jetis#Hotel Tentrem Yogyakarta#1#https://cdn.discordapp.com/attachments/743422487882104837/1150987888553623653/image.png#3471#https://www.google.com/maps?q=Hotel+Tentrem+Yogyakarta&source=lmns&entry=mc&bih=1115&biw=2124&hl=en-US&sa=X&ved=2ahUKEwjjl-HHiKSBAxUu5jgGHTU3BiwQ0pQJKAJ6BAgBEAY#1 7#other#Jl. P. Mangkubumi No.72A Cokrodiningratan Kec. Jetis#Hotel Tentrem Yogyakarta#1#https://cdn.discordapp.com/attachments/743422487882104837/1150987888553623653/image.png#3471#https://www.google.com/maps?q=Hotel+Tentrem+Yogyakarta&source=lmns&entry=mc&bih=1115&biw=2124&hl=en-US&sa=X&ved=2ahUKEwjjl-HHiKSBAxUu5jgGHTU3BiwQ0pQJKAJ6BAgBEAY#1#false
9 8#Moluo Kec.Kwandang#Pulau Saronde Gorontalo#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0d/ec/58/21/saronde-island-a-place.jpg?w=700&h=-1&s=1#7505#https://www.google.com/maps/place/Pulau+Saronde+Gorontalo/@0.9263376,122.8613201,17z/data=!3m1!4b1!4m6!3m5!1s0x32795bf34dff4467:0xa8beb2a832ae8176!8m2!3d0.9263376!4d122.863895!16s%2Fg%2F11l241cc1d?hl=id&entry=ttu#1 8#other#Moluo Kec.Kwandang#Pulau Saronde Gorontalo#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0d/ec/58/21/saronde-island-a-place.jpg?w=700&h=-1&s=1#7505#https://www.google.com/maps/place/Pulau+Saronde+Gorontalo/@0.9263376,122.8613201,17z/data=!3m1!4b1!4m6!3m5!1s0x32795bf34dff4467:0xa8beb2a832ae8176!8m2!3d0.9263376!4d122.863895!16s%2Fg%2F11l241cc1d?hl=id&entry=ttu#1#false
10 9#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu#1 9#beach#Dusun Katiet Desa Bosua Kecamatan Sipora#Pantai Katiet#1#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/d7/fe/f4/mentawai-islands.jpg?w=500&h=-1&s=1#1301#https://www.google.com/maps/place/Katiet,+Bosua,+Sipora+Selatan,+Mentawai+Islands+Regency,+West+Sumatra/@-2.375793,99.848187,15z/data=!3m1!4b1!4m6!3m5!1s0x2fd27efa8363912f:0x8c9c19bd76cba179!8m2!3d-2.375793!4d99.848187!16s%2Fg%2F1tcwz0mt?hl=en-US&entry=ttu#1#false
11 10#Pulau Padar#Pulau Padar#2#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/18/4e/11/f3/padar-island.jpg?w=800&h=-1&s=1#5315#https://www.google.com/maps/place/Pulau+Padar/@-8.6554183,119.570686,14.83z/data=!4m6!3m5!1s0x2db4f84ff6cd01ab:0xf7e6fd33b692a898!8m2!3d-8.6489909!4d119.5832593!16s%2Fg%2F121d16wd?entry=ttu#1 10#other#Pulau Padar#Pulau Padar#2#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/18/4e/11/f3/padar-island.jpg?w=800&h=-1&s=1#5315#https://www.google.com/maps/place/Pulau+Padar/@-8.6554183,119.570686,14.83z/data=!4m6!3m5!1s0x2db4f84ff6cd01ab:0xf7e6fd33b692a898!8m2!3d-8.6489909!4d119.5832593!16s%2Fg%2F121d16wd?entry=ttu#1#false
12 11#Jl. Ahmad Yani 18, Kel. Kelimutu, Kec. Ende Tengah#Sari rasa#3#https://cdn.discordapp.com/attachments/743422487882104837/1151903310295601172/image.png#5315#https://www.google.com/maps/place/Rumah+Makan+Cha+Cha/@-8.6153697,120.4652533,21z/data=!4m6!3m5!1s0x2db37469e54c0dd3:0x36ac988c726ed544!8m2!3d-8.6153631!4d120.465275!16s%2Fg%2F11bwpclp91?entry=ttu#1 11#culinary#Jl. Ahmad Yani 18, Kel. Kelimutu, Kec. Ende Tengah#Sari rasa#3#https://cdn.discordapp.com/attachments/743422487882104837/1151903310295601172/image.png#5315#https://www.google.com/maps/place/Rumah+Makan+Cha+Cha/@-8.6153697,120.4652533,21z/data=!4m6!3m5!1s0x2db37469e54c0dd3:0x36ac988c726ed544!8m2!3d-8.6153631!4d120.465275!16s%2Fg%2F11bwpclp91?entry=ttu#1#false
13 12#Danau Sentani#Danau Sentani#3#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/c7/e8/17/20190630-143008-largejpg.jpg?w=800&h=-1&s=1#9403#https://www.google.com/maps/place/Danau+Sentani/@-2.639976,140.3889748,12.46z/data=!4m7!3m6!1s0x686cf33c72660fbf:0x7e5c1e7d20d930d7!4b1!8m2!3d-2.6133004!4d140.518734!16s%2Fm%2F0tkjf6v?entry=ttu#1 12#other#Danau Sentani#Danau Sentani#3#https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/c7/e8/17/20190630-143008-largejpg.jpg?w=800&h=-1&s=1#9403#https://www.google.com/maps/place/Danau+Sentani/@-2.639976,140.3889748,12.46z/data=!4m7!3m6!1s0x686cf33c72660fbf:0x7e5c1e7d20d930d7!4b1!8m2!3d-2.6133004!4d140.518734!16s%2Fm%2F0tkjf6v?entry=ttu#1#false

View File

@ -1,4 +1,4 @@
id,username,password id,username,password
1,user123,password 1,user123,$2a$10$5jbejbMXCs7noqPBt1v9K.TCjnqDp.GtTkMiRRRwU8/t78GiGjY9a
2,critics,password 2,critics,$2a$10$5jbejbMXCs7noqPBt1v9K.TCjnqDp.GtTkMiRRRwU8/t78GiGjY9a
3,plebs,password 3,plebs,$2a$10$5jbejbMXCs7noqPBt1v9K.TCjnqDp.GtTkMiRRRwU8/t78GiGjY9a
1 id username password
2 1 user123 password $2a$10$5jbejbMXCs7noqPBt1v9K.TCjnqDp.GtTkMiRRRwU8/t78GiGjY9a
3 2 critics password $2a$10$5jbejbMXCs7noqPBt1v9K.TCjnqDp.GtTkMiRRRwU8/t78GiGjY9a
4 3 plebs password $2a$10$5jbejbMXCs7noqPBt1v9K.TCjnqDp.GtTkMiRRRwU8/t78GiGjY9a

View File

@ -13,3 +13,4 @@ DROP TABLE IF EXISTS users;
DROP TYPE IF EXISTS user_reports_type; DROP TYPE IF EXISTS user_reports_type;
DROP TYPE IF EXISTS comment_type; DROP TYPE IF EXISTS comment_type;
DROP TYPE IF EXISTS location_type;

View File

@ -53,14 +53,14 @@ CREATE TABLE user_reports(
CREATE TABLE regions( CREATE TABLE regions(
"id" serial primary key not null, "id" serial primary key not null,
"region_name" varchar, "region_name" varchar not null,
"created_at" timestamp default(now()), "created_at" timestamp default(now()),
"updated_at" timestamp default(now()) "updated_at" timestamp default(now())
); );
CREATE TABLE provinces( CREATE TABLE provinces(
"id" serial primary key not null, "id" serial primary key not null,
"province_name" varchar, "province_name" varchar not null,
"region_id" smallint references "regions"("id") not null, "region_id" smallint references "regions"("id") not null,
"created_at" timestamp default(now()), "created_at" timestamp default(now()),
"updated_at" timestamp default(now()) "updated_at" timestamp default(now())
@ -68,22 +68,31 @@ CREATE TABLE provinces(
CREATE TABLE regencies( CREATE TABLE regencies(
"id" serial primary key not null, "id" serial primary key not null,
"regency_name" varchar, "regency_name" varchar not null,
"province_id" smallint references "provinces"("id") not null, "province_id" smallint references "provinces"("id") not null,
"created_at" timestamp default(now()), "created_at" timestamp default(now()),
"updated_at" timestamp default(now()) "updated_at" timestamp default(now())
); );
CREATE TYPE location_type AS ENUM(
'beach',
'amusement park',
'culinary',
'hiking / camping',
'other'
);
CREATE TABLE locations( CREATE TABLE locations(
"id" serial primary key not null, "id" serial primary key not null,
"address" varchar not null, "address" varchar not null,
"name" varchar not null, "name" varchar not null,
"google_maps_link" varchar, "google_maps_link" varchar,
"location_type" location_type not null,
"submitted_by" integer references "users"("id") not null, "submitted_by" integer references "users"("id") not null,
"total_visited" integer, "total_visited" integer,
"thumbnail" varchar, "thumbnail" varchar,
"regency_id" smallint references "regencies"("id") not null, "regency_id" smallint references "regencies"("id") not null,
"is_deleted" boolean, "is_deleted" boolean not null,
"created_at" timestamp default(now()), "created_at" timestamp default(now()),
"updated_at" timestamp default(now()) "updated_at" timestamp default(now())
); );
@ -112,6 +121,7 @@ CREATE TABLE reviews (
"comments" text not null, "comments" text not null,
"score" smallint not null, "score" smallint not null,
"is_from_critic" boolean not null, "is_from_critic" boolean not null,
"cost_approx" integer,
"is_hided" boolean not null default(false), -- if comments violate TOS just hide the reviews "is_hided" boolean not null default(false), -- if comments violate TOS just hide the reviews
"location_id" integer references "locations"("id") not null, "location_id" integer references "locations"("id") not null,
"created_at" timestamp default(now()), "created_at" timestamp default(now()),

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS user_follow;

View File

@ -0,0 +1,7 @@
CREATE TABLE user_follow(
id serial primary key not null,
follower_id integer references "users"("id") not null,
followee_id integer references "users"("id") not null,
created_at timestamp default (now()) not null,
updated_at timestamp default (now()) not null
)

View File

@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN about;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN about text;

View File

@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN IF EXISTS website;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN website varchar;

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS news_events;

View File

@ -0,0 +1,13 @@
CREATE TABLE news_events (
"id" serial primary key not null,
"title" varchar not null,
"url" varchar not null,
"source" varchar not null,
"thumbnail" varchar,
"description" text,
"is_deleted" boolean not null default(false),
"submitted_by" int references "users"("id") not null,
"approved_by" int references "users"("id"),
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
)

View File

@ -6,6 +6,7 @@ package mockdb
import ( import (
context "context" context "context"
sql "database/sql"
reflect "reflect" reflect "reflect"
db "git.nochill.in/nochill/hiling_go/db/sqlc" db "git.nochill.in/nochill/hiling_go/db/sqlc"
@ -35,6 +36,20 @@ func (m *MockStore) EXPECT() *MockStoreMockRecorder {
return m.recorder return m.recorder
} }
// AddFollowUser mocks base method.
func (m *MockStore) AddFollowUser(arg0 context.Context, arg1 db.AddFollowUserParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddFollowUser", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// AddFollowUser indicates an expected call of AddFollowUser.
func (mr *MockStoreMockRecorder) AddFollowUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFollowUser", reflect.TypeOf((*MockStore)(nil).AddFollowUser), arg0, arg1)
}
// CheckIfReviewExists mocks base method. // CheckIfReviewExists mocks base method.
func (m *MockStore) CheckIfReviewExists(arg0 context.Context, arg1 db.CheckIfReviewExistsParams) (int64, error) { func (m *MockStore) CheckIfReviewExists(arg0 context.Context, arg1 db.CheckIfReviewExistsParams) (int64, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -50,20 +65,63 @@ func (mr *MockStoreMockRecorder) CheckIfReviewExists(arg0, arg1 interface{}) *go
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckIfReviewExists", reflect.TypeOf((*MockStore)(nil).CheckIfReviewExists), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckIfReviewExists", reflect.TypeOf((*MockStore)(nil).CheckIfReviewExists), arg0, arg1)
} }
// CreateLocation mocks base method. // CreateImage mocks base method.
func (m *MockStore) CreateLocation(arg0 context.Context, arg1 db.CreateLocationParams) error { func (m *MockStore) CreateImage(arg0 context.Context, arg1 []db.CreateImageParams) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateLocation", arg0, arg1) ret := m.ctrl.Call(m, "CreateImage", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// CreateImage indicates an expected call of CreateImage.
func (mr *MockStoreMockRecorder) CreateImage(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateImage", reflect.TypeOf((*MockStore)(nil).CreateImage), arg0, arg1)
}
// CreateLocation mocks base method.
func (m *MockStore) CreateLocation(arg0 context.Context, arg1 db.CreateLocationParams) (int32, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateLocation", arg0, arg1)
ret0, _ := ret[0].(int32)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateLocation indicates an expected call of CreateLocation. // CreateLocation indicates an expected call of CreateLocation.
func (mr *MockStoreMockRecorder) CreateLocation(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStoreMockRecorder) CreateLocation(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLocation", reflect.TypeOf((*MockStore)(nil).CreateLocation), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLocation", reflect.TypeOf((*MockStore)(nil).CreateLocation), arg0, arg1)
} }
// CreateLocationTx mocks base method.
func (m *MockStore) CreateLocationTx(arg0 context.Context, arg1 db.CreateLocationTxParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateLocationTx", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// CreateLocationTx indicates an expected call of CreateLocationTx.
func (mr *MockStoreMockRecorder) CreateLocationTx(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLocationTx", reflect.TypeOf((*MockStore)(nil).CreateLocationTx), arg0, arg1)
}
// CreateNewsEvents mocks base method.
func (m *MockStore) CreateNewsEvents(arg0 context.Context, arg1 db.CreateNewsEventsParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateNewsEvents", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// CreateNewsEvents indicates an expected call of CreateNewsEvents.
func (mr *MockStoreMockRecorder) CreateNewsEvents(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNewsEvents", reflect.TypeOf((*MockStore)(nil).CreateNewsEvents), arg0, arg1)
}
// CreateReview mocks base method. // CreateReview mocks base method.
func (m *MockStore) CreateReview(arg0 context.Context, arg1 db.CreateReviewParams) (db.Review, error) { func (m *MockStore) CreateReview(arg0 context.Context, arg1 db.CreateReviewParams) (db.Review, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -169,6 +227,21 @@ func (mr *MockStoreMockRecorder) GetListLocations(arg0 interface{}) *gomock.Call
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)
} }
// GetListProvinces mocks base method.
func (m *MockStore) GetListProvinces(arg0 context.Context) ([]db.GetListProvincesRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetListProvinces", arg0)
ret0, _ := ret[0].([]db.GetListProvincesRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetListProvinces indicates an expected call of GetListProvinces.
func (mr *MockStoreMockRecorder) GetListProvinces(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListProvinces", reflect.TypeOf((*MockStore)(nil).GetListProvinces), arg0)
}
// GetListRecentLocationsWithRatings mocks base method. // GetListRecentLocationsWithRatings mocks base method.
func (m *MockStore) GetListRecentLocationsWithRatings(arg0 context.Context, arg1 int32) ([]db.GetListRecentLocationsWithRatingsRow, error) { func (m *MockStore) GetListRecentLocationsWithRatings(arg0 context.Context, arg1 int32) ([]db.GetListRecentLocationsWithRatingsRow, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -184,6 +257,36 @@ func (mr *MockStoreMockRecorder) GetListRecentLocationsWithRatings(arg0, arg1 in
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListRecentLocationsWithRatings", reflect.TypeOf((*MockStore)(nil).GetListRecentLocationsWithRatings), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListRecentLocationsWithRatings", reflect.TypeOf((*MockStore)(nil).GetListRecentLocationsWithRatings), arg0, arg1)
} }
// GetListRegencies mocks base method.
func (m *MockStore) GetListRegencies(arg0 context.Context) ([]db.GetListRegenciesRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetListRegencies", arg0)
ret0, _ := ret[0].([]db.GetListRegenciesRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetListRegencies indicates an expected call of GetListRegencies.
func (mr *MockStoreMockRecorder) GetListRegencies(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListRegencies", reflect.TypeOf((*MockStore)(nil).GetListRegencies), arg0)
}
// GetListRegions mocks base method.
func (m *MockStore) GetListRegions(arg0 context.Context) ([]db.GetListRegionsRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetListRegions", arg0)
ret0, _ := ret[0].([]db.GetListRegionsRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetListRegions indicates an expected call of GetListRegions.
func (mr *MockStoreMockRecorder) GetListRegions(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListRegions", reflect.TypeOf((*MockStore)(nil).GetListRegions), arg0)
}
// GetLocation mocks base method. // GetLocation mocks base method.
func (m *MockStore) GetLocation(arg0 context.Context, arg1 int32) (db.GetLocationRow, error) { func (m *MockStore) GetLocation(arg0 context.Context, arg1 int32) (db.GetLocationRow, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -214,6 +317,21 @@ func (mr *MockStoreMockRecorder) GetLocationTag(arg0, arg1 interface{}) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLocationTag", reflect.TypeOf((*MockStore)(nil).GetLocationTag), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLocationTag", reflect.TypeOf((*MockStore)(nil).GetLocationTag), arg0, arg1)
} }
// GetNewsEventsList mocks base method.
func (m *MockStore) GetNewsEventsList(arg0 context.Context, arg1 db.GetNewsEventsListParams) ([]db.NewsEventRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNewsEventsList", arg0, arg1)
ret0, _ := ret[0].([]db.NewsEventRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetNewsEventsList indicates an expected call of GetNewsEventsList.
func (mr *MockStoreMockRecorder) GetNewsEventsList(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNewsEventsList", reflect.TypeOf((*MockStore)(nil).GetNewsEventsList), arg0, arg1)
}
// GetSession mocks base method. // GetSession mocks base method.
func (m *MockStore) GetSession(arg0 context.Context, arg1 int32) (db.UserSession, error) { func (m *MockStore) GetSession(arg0 context.Context, arg1 int32) (db.UserSession, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -274,6 +392,64 @@ func (mr *MockStoreMockRecorder) GetUserReviewByLocation(arg0, arg1 interface{})
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserReviewByLocation", reflect.TypeOf((*MockStore)(nil).GetUserReviewByLocation), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserReviewByLocation", reflect.TypeOf((*MockStore)(nil).GetUserReviewByLocation), arg0, arg1)
} }
// GetUserStats mocks base method.
func (m *MockStore) GetUserStats(arg0 context.Context, arg1 int32) (db.GetUserStatsRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserStats", arg0, arg1)
ret0, _ := ret[0].(db.GetUserStatsRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserStats indicates an expected call of GetUserStats.
func (mr *MockStoreMockRecorder) GetUserStats(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStats", reflect.TypeOf((*MockStore)(nil).GetUserStats), arg0, arg1)
}
// RemoveFollowUser mocks base method.
func (m *MockStore) RemoveFollowUser(arg0 context.Context, arg1 db.RemoveFollowUserParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveFollowUser", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveFollowUser indicates an expected call of RemoveFollowUser.
func (mr *MockStoreMockRecorder) RemoveFollowUser(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveFollowUser", reflect.TypeOf((*MockStore)(nil).RemoveFollowUser), arg0, arg1)
}
// UpdateAvatar mocks base method.
func (m *MockStore) UpdateAvatar(arg0 context.Context, arg1 db.UpdateAvatarParams) (sql.NullString, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAvatar", arg0, arg1)
ret0, _ := ret[0].(sql.NullString)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateAvatar indicates an expected call of UpdateAvatar.
func (mr *MockStoreMockRecorder) UpdateAvatar(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAvatar", reflect.TypeOf((*MockStore)(nil).UpdateAvatar), arg0, arg1)
}
// UpdateLocationThumbnail mocks base method.
func (m *MockStore) UpdateLocationThumbnail(arg0 context.Context, arg1 db.UpdateLocationThumbnailParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateLocationThumbnail", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateLocationThumbnail indicates an expected call of UpdateLocationThumbnail.
func (mr *MockStoreMockRecorder) UpdateLocationThumbnail(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLocationThumbnail", reflect.TypeOf((*MockStore)(nil).UpdateLocationThumbnail), arg0, arg1)
}
// UpdatePassword mocks base method. // UpdatePassword mocks base method.
func (m *MockStore) UpdatePassword(arg0 context.Context, arg1 db.UpdatePasswordParams) error { func (m *MockStore) UpdatePassword(arg0 context.Context, arg1 db.UpdatePasswordParams) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -289,10 +465,10 @@ func (mr *MockStoreMockRecorder) UpdatePassword(arg0, arg1 interface{}) *gomock.
} }
// UpdateUser mocks base method. // UpdateUser mocks base method.
func (m *MockStore) UpdateUser(arg0 context.Context, arg1 db.UpdateUserParams) (db.User, error) { func (m *MockStore) UpdateUser(arg0 context.Context, arg1 db.UpdateUserParams) (db.UpdateUserRow, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", arg0, arg1) ret := m.ctrl.Call(m, "UpdateUser", arg0, arg1)
ret0, _ := ret[0].(db.User) ret0, _ := ret[0].(db.UpdateUserRow)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }

10
db/queries/follow.sql Normal file
View File

@ -0,0 +1,10 @@
-- name: AddFollowUser :exec
INSERT INTO user_follow (
follower_id,
followee_id
) VALUES($1, $2);
-- name: RemoveFollowUser :exec
DELETE FROM
user_follow
WHERE follower_id = $1 AND followee_id = $2;

View File

@ -19,17 +19,6 @@ WHERE approved_by IS NOT NULL
ORDER BY l.created_at ASC ORDER BY l.created_at ASC
LIMIT $1; LIMIT $1;
-- name: CreateLocation :exec
INSERT INTO locations(
address,
name,
submitted_by,
regency_id,
google_maps_link
) values (
$1, $2, $3, $4, $5
);
-- name: GetLocationTag :many -- name: GetLocationTag :many
SELECT SELECT
name name
@ -39,3 +28,9 @@ AND
target_id = $1 target_id = $1
AND AND
approved_by IS NOT NULL; approved_by IS NOT NULL;
-- name: UpdateLocationThumbnail :exec
UPDATE locations
SET thumbnail = $1
WHERE id = $2;

View File

@ -0,0 +1,8 @@
-- name: CreateNewsEvents :exec
INSERT INTO news_events(
title,
url,
source,
description,
submitted_by
) VALUES ( $1, $2, $3, $4, $5);

6
db/queries/provinces.sql Normal file
View File

@ -0,0 +1,6 @@
-- name: GetListProvinces :many
SELECT
id,
province_name,
region_id
FROM PROVINCES;

6
db/queries/regencies.sql Normal file
View File

@ -0,0 +1,6 @@
-- name: GetListRegencies :many
SELECT
id,
regency_name,
province_id
FROM regencies;

5
db/queries/regions.sql Normal file
View File

@ -0,0 +1,5 @@
-- name: GetListRegions :many
SELECT
id,
region_name
FROM regions;

View File

@ -5,17 +5,13 @@ INSERT INTO users (
) VALUES ($1, $2) ) VALUES ($1, $2)
RETURNING *; RETURNING *;
-- name: UpdateUser :one
UPDATE users
SET
email = COALESCE(sqlc.narg(email), email),
username = COALESCE(sqlc.narg(username), username),
avatar_picture = COALESCE(sqlc.narg(avatar_picture), avatar_picture)
WHERE
id = sqlc.arg(id)
RETURNING *;
-- name: UpdatePassword :exec -- name: UpdatePassword :exec
UPDATE users UPDATE users
SET password = $1 SET password = $1
WHERE id = $2; WHERE id = $2;
-- name: UpdateAvatar :one
UPDATE users
SET avatar_picture = $1
WHERE id = $2
RETURNING avatar_picture;

43
db/sqlc/follow.sql.go Normal file
View File

@ -0,0 +1,43 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
// source: follow.sql
package db
import (
"context"
)
const addFollowUser = `-- name: AddFollowUser :exec
INSERT INTO user_follow (
follower_id,
followee_id
) VALUES($1, $2)
`
type AddFollowUserParams struct {
FollowerID int32 `json:"follower_id"`
FolloweeID int32 `json:"followee_id"`
}
func (q *Queries) AddFollowUser(ctx context.Context, arg AddFollowUserParams) error {
_, err := q.db.ExecContext(ctx, addFollowUser, arg.FollowerID, arg.FolloweeID)
return err
}
const removeFollowUser = `-- name: RemoveFollowUser :exec
DELETE FROM
user_follow
WHERE follower_id = $1 AND followee_id = $2
`
type RemoveFollowUserParams struct {
FollowerID int32 `json:"follower_id"`
FolloweeID int32 `json:"followee_id"`
}
func (q *Queries) RemoveFollowUser(ctx context.Context, arg RemoveFollowUserParams) error {
_, err := q.db.ExecContext(ctx, removeFollowUser, arg.FollowerID, arg.FolloweeID)
return err
}

View File

@ -2,7 +2,10 @@ package db
import ( import (
"context" "context"
"strings"
"time" "time"
"git.nochill.in/nochill/hiling_go/util"
) )
type GetImagesByLocationParams struct { type GetImagesByLocationParams struct {
@ -59,3 +62,38 @@ func (q *Queries) GetImagesByLocation(ctx context.Context, arg GetImagesByLocati
} }
return items, nil return items, nil
} }
type CreateImageParams struct {
ImageUrl string `json:"url"`
UploadedBy int32 `json:"uploaded_by"`
ImageType string `json:"image_type"`
ImageOf int32 `json:"image_of"`
}
func (q *Queries) CreateImage(ctx context.Context, arg []CreateImageParams) error {
values := []interface{}{}
queryStr := ` INSERT INTO
images(image_url, uploaded_by, image_type, image_of)
VALUES
`
for _, row := range arg {
queryStr += "(?, ?, ?, ?),"
values = append(values, row.ImageUrl, row.UploadedBy, row.ImageType, row.ImageOf)
}
queryStr = strings.TrimSuffix(queryStr, ",")
// Replacing ? with $n for postgres
queryStr = util.ReplaceSQL(queryStr, "?")
// prepare the statement
stmt, _ := q.db.PrepareContext(ctx, queryStr)
// format all vals at once
_, err := stmt.ExecContext(ctx, values...)
return err
}

View File

@ -46,9 +46,9 @@ func (q *Queries) GetTopListLocations(ctx context.Context, arg GetTopListLocatio
FROM( FROM(
SELECT SELECT
*, *,
(SELECT 5 * 4 + COALESCE(critic_score, 0) * COALESCE(critic_count, 0) / 5 + COALESCE(critic_count, 0)) as critic_bayes, (SELECT 5 * 5 + COALESCE(critic_score, 0) * COALESCE(critic_count, 0) / 5 + COALESCE(critic_count, 0)) as critic_bayes,
(SELECT 50 + COALESCE(user_score, 0) * COALESCE(user_count, 0) / 50 + COALESCE(user_count, 0)) as user_bayes, (SELECT 5 * 5 + COALESCE(user_score, 0) * COALESCE(user_count, 0) / 5 + COALESCE(user_count, 0)) as user_bayes,
((SELECT 50 + COALESCE(user_score, 0) * COALESCE(user_count, 0) / 50 + COALESCE(user_count, 0)) + (SELECT 5 * 4 + COALESCE(critic_score, 0) * COALESCE(critic_count, 0) / 5 + COALESCE(critic_count, 0)) ) / 2 as avg_bayes ((SELECT 5 * 5 + COALESCE(user_score, 0) * COALESCE(user_count, 0) / 50 + COALESCE(user_count, 0)) + (SELECT 5 * 5 + COALESCE(critic_score, 0) * COALESCE(critic_count, 0) / 5 + COALESCE(critic_count, 0)) ) / 2 as avg_bayes
FROM ( FROM (
SELECT SELECT
@ -171,3 +171,53 @@ func (q *Queries) GetLocation(ctx context.Context, location_id int32) (GetLocati
return i, err return i, err
} }
const createLocation = `-- name: CreateLocation :exec
INSERT INTO locations(
address,
name,
submitted_by,
location_type,
regency_id,
google_maps_link,
approved_by,
is_deleted
) values (
$1, $2, $3, $4, $5, $6, $7, $8
)
RETURNING id
`
type CreateLocationParams struct {
Address string `json:"address"`
Name string `json:"name"`
SubmittedBy int32 `json:"submitted_by"`
LocationType LocationType `json:"location_type"`
RegencyID int16 `json:"regency_id"`
GoogleMapsLink sql.NullString `json:"google_maps_link"`
IsDeleted bool `json:"is_deleted"`
ApprovedBy sql.NullInt32 `json:"approved_by"`
}
func (q *Queries) CreateLocation(ctx context.Context, arg CreateLocationParams) (int32, error) {
row := q.db.QueryRowContext(ctx, createLocation,
arg.Address,
arg.Name,
arg.SubmittedBy,
arg.LocationType,
arg.RegencyID,
arg.GoogleMapsLink,
arg.ApprovedBy,
arg.IsDeleted,
)
var i int32
err := row.Scan(
&i,
)
fmt.Println(i)
return i, err
}

View File

@ -10,39 +10,8 @@ import (
"database/sql" "database/sql"
) )
const createLocation = `-- name: CreateLocation :exec
INSERT INTO locations(
address,
name,
submitted_by,
regency_id,
google_maps_link
) values (
$1, $2, $3, $4, $5
)
`
type CreateLocationParams struct {
Address string `json:"address"`
Name string `json:"name"`
SubmittedBy int32 `json:"submitted_by"`
RegencyID int16 `json:"regency_id"`
GoogleMapsLink sql.NullString `json:"google_maps_link"`
}
func (q *Queries) CreateLocation(ctx context.Context, arg CreateLocationParams) error {
_, err := q.db.ExecContext(ctx, createLocation,
arg.Address,
arg.Name,
arg.SubmittedBy,
arg.RegencyID,
arg.GoogleMapsLink,
)
return err
}
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, location_type, submitted_by, total_visited, thumbnail, regency_id, is_deleted, created_at, updated_at, approved_by, approved_at FROM locations
` `
func (q *Queries) GetListLocations(ctx context.Context) ([]Location, error) { func (q *Queries) GetListLocations(ctx context.Context) ([]Location, error) {
@ -59,6 +28,7 @@ func (q *Queries) GetListLocations(ctx context.Context) ([]Location, error) {
&i.Address, &i.Address,
&i.Name, &i.Name,
&i.GoogleMapsLink, &i.GoogleMapsLink,
&i.LocationType,
&i.SubmittedBy, &i.SubmittedBy,
&i.TotalVisited, &i.TotalVisited,
&i.Thumbnail, &i.Thumbnail,
@ -179,3 +149,19 @@ func (q *Queries) GetLocationTag(ctx context.Context, targetID int32) ([]string,
} }
return items, nil return items, nil
} }
const updateLocationThumbnail = `-- name: UpdateLocationThumbnail :exec
UPDATE locations
SET thumbnail = $1
WHERE id = $2
`
type UpdateLocationThumbnailParams struct {
Thumbnail sql.NullString `json:"thumbnail"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateLocationThumbnail(ctx context.Context, arg UpdateLocationThumbnailParams) error {
_, err := q.db.ExecContext(ctx, updateLocationThumbnail, arg.Thumbnail, arg.ID)
return err
}

View File

@ -57,6 +57,51 @@ func (ns NullCommentType) Value() (driver.Value, error) {
return string(ns.CommentType), nil return string(ns.CommentType), nil
} }
type LocationType string
const (
LocationTypeBeach LocationType = "beach"
LocationTypeAmusementpark LocationType = "amusement park"
LocationTypeCulinary LocationType = "culinary"
LocationTypeHikingCamping LocationType = "hiking / camping"
LocationTypeOther LocationType = "other"
)
func (e *LocationType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = LocationType(s)
case string:
*e = LocationType(s)
default:
return fmt.Errorf("unsupported scan type for LocationType: %T", src)
}
return nil
}
type NullLocationType struct {
LocationType LocationType `json:"location_type"`
Valid bool `json:"valid"` // Valid is true if LocationType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullLocationType) Scan(value interface{}) error {
if value == nil {
ns.LocationType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.LocationType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullLocationType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.LocationType), nil
}
type UserReportsType string type UserReportsType string
const ( const (
@ -140,11 +185,12 @@ type Location struct {
Address string `json:"address"` Address string `json:"address"`
Name string `json:"name"` Name string `json:"name"`
GoogleMapsLink sql.NullString `json:"google_maps_link"` GoogleMapsLink sql.NullString `json:"google_maps_link"`
LocationType LocationType `json:"location_type"`
SubmittedBy int32 `json:"submitted_by"` SubmittedBy int32 `json:"submitted_by"`
TotalVisited sql.NullInt32 `json:"total_visited"` TotalVisited sql.NullInt32 `json:"total_visited"`
Thumbnail sql.NullString `json:"thumbnail"` Thumbnail sql.NullString `json:"thumbnail"`
RegencyID int16 `json:"regency_id"` RegencyID int16 `json:"regency_id"`
IsDeleted sql.NullBool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
ApprovedBy sql.NullInt32 `json:"approved_by"` ApprovedBy sql.NullInt32 `json:"approved_by"`
@ -160,9 +206,23 @@ type LocationImage struct {
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
type NewsEvent struct {
ID int32 `json:"id"`
Title string `json:"title"`
Url string `json:"url"`
Source string `json:"source"`
Thumbnail sql.NullString `json:"thumbnail"`
Description sql.NullString `json:"description"`
IsDeleted bool `json:"is_deleted"`
SubmittedBy int32 `json:"submitted_by"`
ApprovedBy sql.NullInt32 `json:"approved_by"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
}
type Province struct { type Province struct {
ID int32 `json:"id"` ID int32 `json:"id"`
ProvinceName sql.NullString `json:"province_name"` ProvinceName string `json:"province_name"`
RegionID int16 `json:"region_id"` RegionID int16 `json:"region_id"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
@ -170,7 +230,7 @@ type Province struct {
type Regency struct { type Regency struct {
ID int32 `json:"id"` ID int32 `json:"id"`
RegencyName sql.NullString `json:"regency_name"` RegencyName string `json:"regency_name"`
ProvinceID int16 `json:"province_id"` ProvinceID int16 `json:"province_id"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
@ -178,7 +238,7 @@ type Regency struct {
type Region struct { type Region struct {
ID int32 `json:"id"` ID int32 `json:"id"`
RegionName sql.NullString `json:"region_name"` RegionName string `json:"region_name"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
} }
@ -189,6 +249,7 @@ type Review struct {
Comments string `json:"comments"` Comments string `json:"comments"`
Score int16 `json:"score"` Score int16 `json:"score"`
IsFromCritic bool `json:"is_from_critic"` IsFromCritic bool `json:"is_from_critic"`
CostApprox sql.NullInt32 `json:"cost_approx"`
IsHided bool `json:"is_hided"` IsHided bool `json:"is_hided"`
LocationID int32 `json:"location_id"` LocationID int32 `json:"location_id"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
@ -222,6 +283,8 @@ type User struct {
SocialMedia pqtype.NullRawMessage `json:"social_media"` SocialMedia pqtype.NullRawMessage `json:"social_media"`
CreatedAt sql.NullTime `json:"created_at"` CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"` UpdatedAt sql.NullTime `json:"updated_at"`
About sql.NullString `json:"about"`
Website sql.NullString `json:"website"`
} }
type UserActivity struct { type UserActivity struct {
@ -235,6 +298,14 @@ type UserActivity struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
type UserFollow struct {
ID int32 `json:"id"`
FollowerID int32 `json:"follower_id"`
FolloweeID int32 `json:"followee_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UserReport struct { type UserReport struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Message string `json:"message"` Message string `json:"message"`

87
db/sqlc/news_events.go Normal file
View File

@ -0,0 +1,87 @@
package db
import (
"context"
"fmt"
"time"
)
type GetNewsEventsListParams struct {
Limit int32
Offset int32
IsWithApproved string
}
type NewsEventRow struct {
ID int32 `json:"id"`
Title string `json:"title"`
Url string `json:"url"`
Source string `json:"source"`
Thumbnail string `json:"thumbnail"`
Description string `json:"description"`
IsDeleted bool `json:"is_deleted"`
SubmittedBy string `json:"submitted_by"`
ApprovedBy int32 `json:"approved_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (q *Queries) GetNewsEventsList(ctx context.Context, arg GetNewsEventsListParams) ([]NewsEventRow, error) {
getNewsEventsListQ := fmt.Sprintf(`
SELECT
n.id,
n.title,
n.url,
n.source,
COALESCE(n.thumbnail, '') as thumbnail,
COALESCE(n.description, '') as description,
n.is_deleted,
u.username as submitted_by,
COALESCE(n.approved_by, 0) as approved_by,
n.created_at,
n.updated_at
FROM news_events n
JOIN users u ON n.submitted_by = u.id
%s
ORDER BY n.created_at DESC
LIMIT $1
OFFSET $2
`, arg.IsWithApproved)
rows, err := q.db.QueryContext(ctx, getNewsEventsListQ, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []NewsEventRow{}
for rows.Next() {
var i NewsEventRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Url,
&i.Source,
&i.Thumbnail,
&i.Description,
&i.IsDeleted,
&i.SubmittedBy,
&i.ApprovedBy,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -0,0 +1,40 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
// source: news_events.sql
package db
import (
"context"
"database/sql"
)
const createNewsEvents = `-- name: CreateNewsEvents :exec
INSERT INTO news_events(
title,
url,
source,
description,
submitted_by
) VALUES ( $1, $2, $3, $4, $5)
`
type CreateNewsEventsParams struct {
Title string `json:"title"`
Url string `json:"url"`
Source string `json:"source"`
Description sql.NullString `json:"description"`
SubmittedBy int32 `json:"submitted_by"`
}
func (q *Queries) CreateNewsEvents(ctx context.Context, arg CreateNewsEventsParams) error {
_, err := q.db.ExecContext(ctx, createNewsEvents,
arg.Title,
arg.Url,
arg.Source,
arg.Description,
arg.SubmittedBy,
)
return err
}

47
db/sqlc/provinces.sql.go Normal file
View File

@ -0,0 +1,47 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
// source: provinces.sql
package db
import (
"context"
)
const getListProvinces = `-- name: GetListProvinces :many
SELECT
id,
province_name,
region_id
FROM PROVINCES
`
type GetListProvincesRow struct {
ID int32 `json:"id"`
ProvinceName string `json:"province_name"`
RegionID int16 `json:"region_id"`
}
func (q *Queries) GetListProvinces(ctx context.Context) ([]GetListProvincesRow, error) {
rows, err := q.db.QueryContext(ctx, getListProvinces)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetListProvincesRow{}
for rows.Next() {
var i GetListProvincesRow
if err := rows.Scan(&i.ID, &i.ProvinceName, &i.RegionID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -6,21 +6,28 @@ package db
import ( import (
"context" "context"
"database/sql"
) )
type Querier interface { type Querier interface {
AddFollowUser(ctx context.Context, arg AddFollowUserParams) error
CheckIfReviewExists(ctx context.Context, arg CheckIfReviewExistsParams) (int64, error) CheckIfReviewExists(ctx context.Context, arg CheckIfReviewExistsParams) (int64, error)
CreateLocation(ctx context.Context, arg CreateLocationParams) error CreateNewsEvents(ctx context.Context, arg CreateNewsEventsParams) error
CreateSession(ctx context.Context, arg CreateSessionParams) (UserSession, error) CreateSession(ctx context.Context, arg CreateSessionParams) (UserSession, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
GetCountImageByLocation(ctx context.Context, imageOf int32) (int64, error) GetCountImageByLocation(ctx context.Context, imageOf int32) (int64, error)
GetListLocations(ctx context.Context) ([]Location, error) GetListLocations(ctx context.Context) ([]Location, error)
GetListProvinces(ctx context.Context) ([]GetListProvincesRow, error)
GetListRecentLocationsWithRatings(ctx context.Context, limit int32) ([]GetListRecentLocationsWithRatingsRow, error) GetListRecentLocationsWithRatings(ctx context.Context, limit int32) ([]GetListRecentLocationsWithRatingsRow, error)
GetListRegencies(ctx context.Context) ([]GetListRegenciesRow, error)
GetListRegions(ctx context.Context) ([]GetListRegionsRow, error)
GetLocationTag(ctx context.Context, targetID int32) ([]string, error) GetLocationTag(ctx context.Context, targetID int32) ([]string, error)
GetSession(ctx context.Context, id int32) (UserSession, error) GetSession(ctx context.Context, id int32) (UserSession, error)
GetUserReviewByLocation(ctx context.Context, arg GetUserReviewByLocationParams) (GetUserReviewByLocationRow, error) GetUserReviewByLocation(ctx context.Context, arg GetUserReviewByLocationParams) (GetUserReviewByLocationRow, error)
RemoveFollowUser(ctx context.Context, arg RemoveFollowUserParams) error
UpdateAvatar(ctx context.Context, arg UpdateAvatarParams) (sql.NullString, error)
UpdateLocationThumbnail(ctx context.Context, arg UpdateLocationThumbnailParams) error
UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
} }
var _ Querier = (*Queries)(nil) var _ Querier = (*Queries)(nil)

47
db/sqlc/regencies.sql.go Normal file
View File

@ -0,0 +1,47 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
// source: regencies.sql
package db
import (
"context"
)
const getListRegencies = `-- name: GetListRegencies :many
SELECT
id,
regency_name,
province_id
FROM regencies
`
type GetListRegenciesRow struct {
ID int32 `json:"id"`
RegencyName string `json:"regency_name"`
ProvinceID int16 `json:"province_id"`
}
func (q *Queries) GetListRegencies(ctx context.Context) ([]GetListRegenciesRow, error) {
rows, err := q.db.QueryContext(ctx, getListRegencies)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetListRegenciesRow{}
for rows.Next() {
var i GetListRegenciesRow
if err := rows.Scan(&i.ID, &i.RegencyName, &i.ProvinceID); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

45
db/sqlc/regions.sql.go Normal file
View File

@ -0,0 +1,45 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
// source: regions.sql
package db
import (
"context"
)
const getListRegions = `-- name: GetListRegions :many
SELECT
id,
region_name
FROM regions
`
type GetListRegionsRow struct {
ID int32 `json:"id"`
RegionName string `json:"region_name"`
}
func (q *Queries) GetListRegions(ctx context.Context) ([]GetListRegionsRow, error) {
rows, err := q.db.QueryContext(ctx, getListRegions)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetListRegionsRow{}
for rows.Next() {
var i GetListRegionsRow
if err := rows.Scan(&i.ID, &i.RegionName); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -3,6 +3,7 @@ package db
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt"
"github.com/yiplee/sqlc" "github.com/yiplee/sqlc"
) )
@ -13,8 +14,14 @@ type Store interface {
GetImagesByLocation(ctx context.Context, arg GetImagesByLocationParams) ([]GetImagesByLocationRow, error) GetImagesByLocation(ctx context.Context, arg GetImagesByLocationParams) ([]GetImagesByLocationRow, error)
GetLocation(ctx context.Context, location_id int32) (GetLocationRow, error) GetLocation(ctx context.Context, location_id int32) (GetLocationRow, error)
GetUser(ctx context.Context, username string) (GetUserRow, error) GetUser(ctx context.Context, username string) (GetUserRow, error)
UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error)
GetUserStats(ctx context.Context, user_id int32) (GetUserStatsRow, error)
CreateReview(ctx context.Context, arg CreateReviewParams) (Review, error) CreateReview(ctx context.Context, arg CreateReviewParams) (Review, error)
GetListLocationReviews(ctx context.Context, arg GetListLocationReviewsParams) ([]GetListLocationReviewsRow, error) GetListLocationReviews(ctx context.Context, arg GetListLocationReviewsParams) ([]GetListLocationReviewsRow, error)
CreateLocation(ctx context.Context, arg CreateLocationParams) (int32, error)
CreateImage(ctx context.Context, arg []CreateImageParams) error
CreateLocationTx(ctx context.Context, arg CreateLocationTxParams) error
GetNewsEventsList(ctx context.Context, arg GetNewsEventsListParams) ([]NewsEventRow, error)
} }
type SQLStore struct { type SQLStore struct {
@ -30,19 +37,19 @@ func NewStore(db *sql.DB) Store {
} }
// TRANSACTION QUERY FUNCTION // TRANSACTION QUERY FUNCTION
// func (store *SQLStore) execTx(ctx context.Context, fn func(*Queries) error) error { func (store *SQLStore) execTx(ctx context.Context, fn func(*Queries) error) error {
// tx, err := store.db.BeginTx(ctx, nil) tx, err := store.db.BeginTx(ctx, nil)
// if err != nil { if err != nil {
// return err return err
// } }
// q := New(tx) q := New(tx)
// err = fn(q) err = fn(q)
// if err != nil { if err != nil {
// if rbErr := tx.Rollback(); rbErr != nil { if rbErr := tx.Rollback(); rbErr != nil {
// return fmt.Errorf("tx err: %v, rb err : %v", err, rbErr) return fmt.Errorf("tx err: %v, rb err : %v", err, rbErr)
// } }
// return err return err
// } }
// return tx.Commit() return tx.Commit()
// } }

View File

@ -31,6 +31,6 @@ func TestCreateLocation(t *testing.T) {
GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(10)}, GoogleMapsLink: sql.NullString{Valid: true, String: util.RandomString(10)},
} }
err := testQueries.CreateLocation(context.Background(), arg) _, err := testQueries.CreateLocation(context.Background(), arg)
require.NoError(t, err) require.NoError(t, err)
} }

59
db/sqlc/tx_location.go Normal file
View File

@ -0,0 +1,59 @@
package db
import (
"context"
"database/sql"
)
type CreateLocationTxParams struct {
Address string `json:"address"`
Name string `json:"name"`
SubmittedBy int32 `json:"submitted_by"`
LocationType LocationType `json:"location_type"`
RegencyID int16 `json:"regency_id"`
GoogleMapsLink sql.NullString `json:"google_maps_link"`
IsDeleted bool `json:"is_deleted"`
ApprovedBy sql.NullInt32 `json:"approved_by"`
Thumbnail []CreateImageParams `json:"thumbnails"`
}
func (store *SQLStore) CreateLocationTx(ctx context.Context, arg CreateLocationTxParams) error {
err := store.execTx(ctx, func(q *Queries) error {
var err error
location_id, err := q.CreateLocation(ctx, CreateLocationParams{
Address: arg.Address,
Name: arg.Name,
SubmittedBy: arg.SubmittedBy,
LocationType: arg.LocationType,
RegencyID: arg.RegencyID,
GoogleMapsLink: arg.GoogleMapsLink,
IsDeleted: arg.IsDeleted,
ApprovedBy: arg.ApprovedBy,
})
if err != nil {
return err
}
if len(arg.Thumbnail) > 0 {
err := q.CreateImage(ctx, arg.Thumbnail)
if err != nil {
return err
}
err = q.UpdateLocationThumbnail(ctx, UpdateLocationThumbnailParams{
Thumbnail: sql.NullString{Valid: true, String: arg.Thumbnail[0].ImageUrl},
ID: location_id,
})
if err != nil {
return err
}
}
return nil
})
return err
}

View File

@ -3,16 +3,21 @@ package db
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"time"
"github.com/sqlc-dev/pqtype" "github.com/sqlc-dev/pqtype"
) )
const getUser = `-- name: GetUser :one const getUserQ = `-- name: GetUser :one
SELECT SELECT
id, id,
COALESCE(email, '') as email, COALESCE(email, '') as email,
password, password,
username, username,
COALESCE(google_sign_in_payload, '') as google_sign_in_payload,
COALESCE(about, '') as about,
COALESCE(website, '') as website,
COALESCE(avatar_picture, '') as avatar_picture, COALESCE(avatar_picture, '') as avatar_picture,
banned_at, banned_at,
banned_until, banned_until,
@ -21,7 +26,9 @@ SELECT
is_admin, is_admin,
is_critics, is_critics,
is_verified, is_verified,
social_media COALESCE(social_media, '[]'),
created_at,
updated_at
FROM USERS FROM USERS
WHERE username = $1 WHERE username = $1
` `
@ -30,7 +37,10 @@ type GetUserRow struct {
ID int32 `json:"id"` ID int32 `json:"id"`
Email string `json:"email"` Email string `json:"email"`
Password string `json:"-"` Password string `json:"-"`
About string `json:"about"`
Website string `json:"website"`
Username string `json:"username"` Username string `json:"username"`
GoogleSignInPayload string `json:"google_sign_in_payload"`
AvatarPicture string `json:"avatar_picture"` AvatarPicture string `json:"avatar_picture"`
BannedAt sql.NullTime `json:"banned_at"` BannedAt sql.NullTime `json:"banned_at"`
BannedUntil sql.NullTime `json:"banned_until"` BannedUntil sql.NullTime `json:"banned_until"`
@ -39,17 +49,22 @@ type GetUserRow struct {
IsAdmin bool `json:"is_admin"` IsAdmin bool `json:"is_admin"`
IsCritics bool `json:"is_critics"` IsCritics bool `json:"is_critics"`
IsVerified bool `json:"is_verified"` IsVerified bool `json:"is_verified"`
SocialMedia pqtype.NullRawMessage `json:"social_media"` SocialMedia json.RawMessage `json:"social_media"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
func (q *Queries) GetUser(ctx context.Context, username string) (GetUserRow, error) { func (q *Queries) GetUser(ctx context.Context, username string) (GetUserRow, error) {
row := q.db.QueryRowContext(ctx, getUser, username) row := q.db.QueryRowContext(ctx, getUserQ, username)
var i GetUserRow var i GetUserRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.Email, &i.Email,
&i.Password, &i.Password,
&i.Username, &i.Username,
&i.GoogleSignInPayload,
&i.About,
&i.Website,
&i.AvatarPicture, &i.AvatarPicture,
&i.BannedAt, &i.BannedAt,
&i.BannedUntil, &i.BannedUntil,
@ -59,6 +74,161 @@ func (q *Queries) GetUser(ctx context.Context, username string) (GetUserRow, err
&i.IsCritics, &i.IsCritics,
&i.IsVerified, &i.IsVerified,
&i.SocialMedia, &i.SocialMedia,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getUserStatsQ = `
SELECT
json_agg(ur.*) AS reviews,
( SELECT COUNT(id) FROM user_follow u WHERE u.followee_id = $1) as followers,
( SELECT COUNT(id) FROM reviews r WHERE r.submitted_by = $1 ) as score_count,
( SELECT
json_agg(r1.*) AS scores_distribution
FROM
(
SELECT
*
FROM
( SELECT COUNT(id) as "0" FROM reviews r WHERE r.score >= 0 AND r.score <= 9 AND r.submitted_by = $1) as score0,
( SELECT COUNT(id) as "1" FROM reviews r WHERE r.score >= 10 AND r.score <= 19 AND r.submitted_by = $1) as score1,
( SELECT COUNT(id) as "2" FROM reviews r WHERE r.score >= 20 AND r.score <= 29 AND r.submitted_by = $1) as score2,
( SELECT COUNT(id) as "3" FROM reviews r WHERE r.score >= 30 AND r.score <= 39 AND r.submitted_by = $1) as score3,
( SELECT COUNT(id) as "4" FROM reviews r WHERE r.score >= 40 AND r.score <= 49 AND r.submitted_by = $1) as score4,
( SELECT COUNT(id) as "5" FROM reviews r WHERE r.score >= 50 AND r.score <= 59 AND r.submitted_by = $1) as score5,
( SELECT COUNT(id) as "6" FROM reviews r WHERE r.score >= 60 AND r.score <= 69 AND r.submitted_by = $1) as score6,
( SELECT COUNT(id) as "7" FROM reviews r WHERE r.score >= 70 AND r.score <= 79 AND r.submitted_by = $1) as score7,
( SELECT COUNT(id) as "8" FROM reviews r WHERE r.score >= 80 AND r.score <= 89 AND r.submitted_by = $1) as score8,
( SELECT COUNT(id) as "9" FROM reviews r WHERE r.score >= 90 AND r.score <= 99 AND r.submitted_by = $1) as score9,
( SELECT COUNT(id) as "99" FROM reviews r WHERE r.score = 100 AND r.submitted_by = $1) as score10
) r1
)
FROM (
SELECT
reviews.id,
reviews.comments,
score,
l.name,
p.province_name,
COALESCE(l.thumbnail, '') as thumbnail
FROM
reviews
JOIN locations l on reviews.location_id = l.id
JOIN regencies r on l.regency_id = r.id
JOIN provinces p on r.province_id = p.id
WHERE reviews.submitted_by = $1
ORDER BY score ASC
LIMIT 10
) ur
`
type GetUserStatsRow struct {
Reviews []byte `json:"-"`
Followers int32 `json:"followers"`
ScoreCount int32 `json:"score_count"`
ScoresDistribution []byte `json:"-"`
}
func (q *Queries) GetUserStats(ctx context.Context, user_id int32) (GetUserStatsRow, error) {
var i GetUserStatsRow
row := q.db.QueryRowContext(ctx, getUserStatsQ, user_id)
err := row.Scan(
&i.Reviews,
&i.Followers,
&i.ScoreCount,
&i.ScoresDistribution,
)
return i, err
}
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET
about = $1,
social_media = $2,
website = $3
WHERE
id = $4
RETURNING
id,
COALESCE(email, ''),
username,
COALESCE(avatar_picture, ''),
COALESCE(about, ''),
COALESCE(website, ''),
COALESCE(google_sign_in_payload, ''),
banned_at,
banned_until,
COALESCE(ban_reason, ''),
is_permaban,
is_admin,
is_critics,
is_verified,
is_active,
COALESCE(social_media, '[]'),
created_at,
updated_at
`
type UpdateUserParams struct {
About sql.NullString `json:"about"`
SocialMedia pqtype.NullRawMessage `json:"social_media"`
Website sql.NullString `json:"website"`
ID int32 `json:"id"`
}
type UpdateUserRow struct {
ID int32 `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
AvatarPicture string `json:"avatar_picture"`
About string `json:"about"`
Website string `json:"website"`
GoogleSignInPayload string `json:"google_sign_in_payload"`
BannedAt sql.NullTime `json:"banned_at"`
BannedUntil sql.NullTime `json:"banned_until"`
BanReason string `json:"ban_reason"`
IsPermaban bool `json:"is_permaban"`
IsAdmin bool `json:"is_admin"`
IsCritics bool `json:"is_critics"`
IsVerified bool `json:"is_verified"`
IsActive bool `json:"is_active"`
SocialMedia json.RawMessage `json:"social_media"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (UpdateUserRow, error) {
row := q.db.QueryRowContext(ctx, updateUser,
arg.About,
arg.SocialMedia,
arg.Website,
arg.ID,
)
var i UpdateUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.AvatarPicture,
&i.About,
&i.Website,
&i.GoogleSignInPayload,
&i.BannedAt,
&i.BannedUntil,
&i.BanReason,
&i.IsPermaban,
&i.IsAdmin,
&i.IsCritics,
&i.IsVerified,
&i.IsActive,
&i.SocialMedia,
&i.CreatedAt,
&i.UpdatedAt,
) )
return i, err return i, err
} }

View File

@ -15,7 +15,7 @@ INSERT INTO users (
username, username,
password password
) VALUES ($1, $2) ) VALUES ($1, $2)
RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, is_active, social_media, created_at, updated_at RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, is_active, social_media, created_at, updated_at, about, website
` `
type CreateUserParams struct { type CreateUserParams struct {
@ -44,10 +44,31 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
&i.SocialMedia, &i.SocialMedia,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.About,
&i.Website,
) )
return i, err return i, err
} }
const updateAvatar = `-- name: UpdateAvatar :one
UPDATE users
SET avatar_picture = $1
WHERE id = $2
RETURNING avatar_picture
`
type UpdateAvatarParams struct {
AvatarPicture sql.NullString `json:"avatar_picture"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateAvatar(ctx context.Context, arg UpdateAvatarParams) (sql.NullString, error) {
row := q.db.QueryRowContext(ctx, updateAvatar, arg.AvatarPicture, arg.ID)
var avatar_picture sql.NullString
err := row.Scan(&avatar_picture)
return avatar_picture, err
}
const updatePassword = `-- name: UpdatePassword :exec const updatePassword = `-- name: UpdatePassword :exec
UPDATE users UPDATE users
SET password = $1 SET password = $1
@ -63,51 +84,3 @@ func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams)
_, err := q.db.ExecContext(ctx, updatePassword, arg.Password, arg.ID) _, err := q.db.ExecContext(ctx, updatePassword, arg.Password, arg.ID)
return err return err
} }
const updateUser = `-- name: UpdateUser :one
UPDATE users
SET
email = COALESCE($1, email),
username = COALESCE($2, username),
avatar_picture = COALESCE($3, avatar_picture)
WHERE
id = $4
RETURNING id, email, username, password, avatar_picture, google_sign_in_payload, banned_at, banned_until, ban_reason, is_permaban, is_admin, is_critics, is_verified, is_active, social_media, created_at, updated_at
`
type UpdateUserParams struct {
Email sql.NullString `json:"email"`
Username sql.NullString `json:"username"`
AvatarPicture sql.NullString `json:"avatar_picture"`
ID int32 `json:"id"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUser,
arg.Email,
arg.Username,
arg.AvatarPicture,
arg.ID,
)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.Password,
&i.AvatarPicture,
&i.GoogleSignInPayload,
&i.BannedAt,
&i.BannedUntil,
&i.BanReason,
&i.IsPermaban,
&i.IsAdmin,
&i.IsCritics,
&i.IsVerified,
&i.IsActive,
&i.SocialMedia,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@ -5,7 +5,7 @@ sudo -u postgres psql \
-c '\copy regions(id, region_name) FROM '"'/tmp/regions.csv'"' DELIMITER '"','"' CSV HEADER;' \ -c '\copy regions(id, region_name) FROM '"'/tmp/regions.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy provinces(id, province_name, region_id) FROM '"'/tmp/provinsi.csv'"' DELIMITER '"','"' CSV HEADER;' \ -c '\copy provinces(id, province_name, region_id) FROM '"'/tmp/provinsi.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regencies(id, province_id, regency_name) FROM '"'/tmp/kabupaten.csv'"' DELIMITER '"','"' CSV HEADER;' \ -c '\copy regencies(id, province_id, regency_name) FROM '"'/tmp/kabupaten.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy locations(id,address,name,submitted_by,thumbnail,regency_id,google_maps_link,approved_by) FROM '"'/tmp/locations.csv'"' DELIMITER '"'#'"' CSV HEADER;' \ -c '\copy locations(id,location_type,address,name,submitted_by,thumbnail,regency_id,google_maps_link,approved_by,is_deleted) FROM '"'/tmp/locations.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-c '\copy reviews(id,submitted_by,is_from_critic,comments,score,is_hided,location_id) FROM '"'/tmp/reviews.csv'"' DELIMITER '"'#'"' CSV HEADER;' \ -c '\copy reviews(id,submitted_by,is_from_critic,comments,score,is_hided,location_id) FROM '"'/tmp/reviews.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-c '\copy images(id,image_url,uploaded_by,image_type,image_of) FROM '"'/tmp/images.csv'"' DELIMITER '"'#'"' CSV HEADER;' \ -c '\copy images(id,image_url,uploaded_by,image_type,image_of) FROM '"'/tmp/images.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-c '\copy tags(id,name,submitted_by,target_id,tags_type) FROM '"'/tmp/tags.csv'"' DELIMITER '"','"' CSV HEADER;' \ -c '\copy tags(id,name,submitted_by,target_id,tags_type) FROM '"'/tmp/tags.csv'"' DELIMITER '"','"' CSV HEADER;' \
@ -15,7 +15,7 @@ sudo -u postgres psql \
-c '\copy regions(id, region_name) FROM '"'/tmp/regions.csv'"' DELIMITER '"','"' CSV HEADER;' \ -c '\copy regions(id, region_name) FROM '"'/tmp/regions.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy provinces(id, province_name, region_id) FROM '"'/tmp/provinsi.csv'"' DELIMITER '"','"' CSV HEADER;' \ -c '\copy provinces(id, province_name, region_id) FROM '"'/tmp/provinsi.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy regencies(id, province_id, regency_name) FROM '"'/tmp/kabupaten.csv'"' DELIMITER '"','"' CSV HEADER;' \ -c '\copy regencies(id, province_id, regency_name) FROM '"'/tmp/kabupaten.csv'"' DELIMITER '"','"' CSV HEADER;' \
-c '\copy locations(id,address,name,submitted_by,thumbnail,regency_id,google_maps_link,approved_by) FROM '"'/tmp/locations.csv'"' DELIMITER '"'#'"' CSV HEADER;' \ -c '\copy locations(id,location_type,address,name,submitted_by,thumbnail,regency_id,google_maps_link,approved_by,is_deleted) FROM '"'/tmp/locations.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-c '\copy reviews(id,submitted_by,is_from_critic,comments,score,is_hided,location_id) FROM '"'/tmp/reviews.csv'"' DELIMITER '"'#'"' CSV HEADER;' \ -c '\copy reviews(id,submitted_by,is_from_critic,comments,score,is_hided,location_id) FROM '"'/tmp/reviews.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-c '\copy images(id,image_url,uploaded_by,image_type,image_of) FROM '"'/tmp/images.csv'"' DELIMITER '"'#'"' CSV HEADER;' \ -c '\copy images(id,image_url,uploaded_by,image_type,image_of) FROM '"'/tmp/images.csv'"' DELIMITER '"'#'"' CSV HEADER;' \
-c '\copy tags(id,name,submitted_by,target_id,tags_type) FROM '"'/tmp/tags.csv'"' DELIMITER '"','"' CSV HEADER;' \ -c '\copy tags(id,name,submitted_by,target_id,tags_type) FROM '"'/tmp/tags.csv'"' DELIMITER '"','"' CSV HEADER;' \

11
notes
View File

@ -104,4 +104,15 @@ PAGINATION
http://andreyzavadskiy.com/2016/12/03/pagination-and-total-number-of-rows-from-one-select/ http://andreyzavadskiy.com/2016/12/03/pagination-and-total-number-of-rows-from-one-select/
##########################################################################################
##########################################################################################
PAGINATION
http://andreyzavadskiy.com/2016/12/03/pagination-and-total-number-of-rows-from-one-select/
https://medium.easyread.co/how-to-do-pagination-in-postgres-with-golang-in-4-common-ways-12365b9fb528?gi=a4683590d1d2
########################################################################################## ##########################################################################################

14
util/common.go Normal file
View File

@ -0,0 +1,14 @@
package util
import (
"strconv"
"strings"
)
func ReplaceSQL(old, searchPattern string) string {
tmpCount := strings.Count(old, searchPattern)
for m := 1; m <= tmpCount; m++ {
old = strings.Replace(old, searchPattern, "$"+strconv.Itoa(m), 1)
}
return old
}