From 17fbc018d5fe0e89302a04f87d57ceb79d36ba90 Mon Sep 17 00:00:00 2001 From: litsynp Date: Tue, 23 Apr 2024 23:57:39 +0900 Subject: [PATCH 01/12] chore: enhance test makefile scripts --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index bc7ecacd..6d5da7c6 100644 --- a/Makefile +++ b/Makefile @@ -60,9 +60,12 @@ run: go run ./cmd/server test: + make db:test:down make db:test:up go test ./... -count=1 -p=1 make db:test:down +test\:run: + go test ./... -count=1 -p=1 ## Database ## db\:up: From 175ea0a87a55adc743705586263e52ff6b109011 Mon Sep 17 00:00:00 2001 From: litsynp Date: Tue, 23 Apr 2024 23:58:03 +0900 Subject: [PATCH 02/12] chore: set up sqlc --- Makefile | 8 + internal/infra/database/database.go | 22 ++ internal/infra/database/gen/db.go | 31 ++ internal/infra/database/gen/models.go | 172 ++++++++++ internal/infra/database/gen/users.sql.go | 390 +++++++++++++++++++++++ internal/infra/database/transaction.go | 23 +- queries/users.sql | 130 ++++++++ sqlc.yaml | 10 + 8 files changed, 779 insertions(+), 7 deletions(-) create mode 100644 internal/infra/database/gen/db.go create mode 100644 internal/infra/database/gen/models.go create mode 100644 internal/infra/database/gen/users.sql.go create mode 100644 queries/users.sql create mode 100644 sqlc.yaml diff --git a/Makefile b/Makefile index 6d5da7c6..6f8721a5 100644 --- a/Makefile +++ b/Makefile @@ -93,3 +93,11 @@ migrate\:down: migrate -path db/migrations -database="${DATABASE_URL}" down migrate\:create: migrate create -ext sql -dir db/migrations -seq $(name) + +## Queries (with sqlc) ## +sqlc\:install: + # sqlc + # https://docs.sqlc.dev/en/latest/ + go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest +sqlc\:generate: + sqlc generate diff --git a/internal/infra/database/database.go b/internal/infra/database/database.go index 8c9f195d..0ffb1e79 100644 --- a/internal/infra/database/database.go +++ b/internal/infra/database/database.go @@ -1,8 +1,10 @@ package database import ( + "context" "database/sql" "errors" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" "github.com/golang-migrate/migrate/v4" @@ -29,6 +31,10 @@ func Open(databaseURL string) (*DB, error) { return &DB{DB: db, databaseURL: databaseURL}, nil } +func New(db DB) *databasegen.Queries { + return databasegen.New(db.DB) +} + func (db *DB) Close() error { return db.DB.Close() } @@ -74,3 +80,19 @@ func (db *DB) Migrate(migrationPath string) error { return nil } + +func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { + return db.DB.ExecContext(ctx, query, args...) +} + +func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { + return db.DB.QueryContext(ctx, query, args...) +} + +func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row { + return db.DB.QueryRowContext(ctx, query, args...) +} + +func (db *DB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { + return db.DB.PrepareContext(ctx, query) +} diff --git a/internal/infra/database/gen/db.go b/internal/infra/database/gen/db.go new file mode 100644 index 00000000..c298514d --- /dev/null +++ b/internal/infra/database/gen/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package databasegen + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/infra/database/gen/models.go b/internal/infra/database/gen/models.go new file mode 100644 index 00000000..75c2e8cd --- /dev/null +++ b/internal/infra/database/gen/models.go @@ -0,0 +1,172 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package databasegen + +import ( + "database/sql" + "encoding/json" + "time" +) + +type BasePost struct { + ID int32 + Title sql.NullString + Content sql.NullString + AuthorID sql.NullInt64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +type Breed struct { + ID int32 + Name string + PetType string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +type Medium struct { + ID int32 + MediaType string + Url string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +type Pet struct { + ID int32 + OwnerID int64 + Name string + PetType string + Sex string + Neutered bool + Breed string + BirthDate time.Time + WeightInKg string + AdditionalNote sql.NullString + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime + ProfileImageID sql.NullInt64 + Remarks string +} + +type ResourceMedium struct { + ID int32 + MediaID sql.NullInt64 + ResourceID sql.NullInt64 + ResourceType sql.NullString + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +type SosCondition struct { + ID int32 + Name sql.NullString + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +type SosDate struct { + ID int32 + DateStartAt sql.NullTime + DateEndAt sql.NullTime + CreatedAt sql.NullTime + UpdatedAt sql.NullTime + DeletedAt sql.NullTime +} + +type SosPost struct { + ID int32 + Title sql.NullString + Content sql.NullString + AuthorID sql.NullInt64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime + Reward sql.NullString + CareType sql.NullString + CarerGender sql.NullString + RewardType sql.NullString + ThumbnailID sql.NullInt64 +} + +type SosPostsCondition struct { + ID int32 + SosPostID sql.NullInt64 + SosConditionID sql.NullInt64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +type SosPostsDate struct { + ID int32 + SosPostID sql.NullInt64 + SosDatesID sql.NullInt64 + CreatedAt sql.NullTime + UpdatedAt sql.NullTime + DeletedAt sql.NullTime +} + +type SosPostsPet struct { + ID int32 + SosPostID sql.NullInt64 + PetID sql.NullInt64 + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +type User struct { + ID int32 + Email string + Password string + Nickname string + Fullname string + FbProviderType sql.NullString + FbUid sql.NullString + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime + ProfileImageID sql.NullInt64 +} + +type VCondition struct { + SosPostID sql.NullInt64 + ConditionsInfo json.RawMessage +} + +type VMediaForSosPost struct { + SosPostID sql.NullInt64 + MediaInfo json.RawMessage +} + +type VPetsForSosPost struct { + SosPostID sql.NullInt64 + PetTypeList interface{} + PetsInfo json.RawMessage +} + +type VSosPost struct { + ID int32 + Title sql.NullString + Content sql.NullString + Reward sql.NullString + RewardType sql.NullString + CareType sql.NullString + CarerGender sql.NullString + ThumbnailID sql.NullInt64 + AuthorID sql.NullInt64 + CreatedAt time.Time + UpdatedAt time.Time + EarliestDateStartAt interface{} + Dates json.RawMessage +} diff --git a/internal/infra/database/gen/users.sql.go b/internal/infra/database/gen/users.sql.go new file mode 100644 index 00000000..5e145413 --- /dev/null +++ b/internal/infra/database/gen/users.sql.go @@ -0,0 +1,390 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: users.sql + +package databasegen + +import ( + "context" + "database/sql" + "time" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users +(email, + nickname, + fullname, + password, + profile_image_id, + fb_provider_type, + fb_uid, + created_at, + updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) +RETURNING id, email, nickname, fullname, profile_image_id, fb_provider_type, fb_uid, created_at, updated_at +` + +type CreateUserParams struct { + Email string + Nickname string + Fullname string + Password string + ProfileImageID sql.NullInt64 + FbProviderType sql.NullString + FbUid sql.NullString +} + +type CreateUserRow struct { + ID int32 + Email string + Nickname string + Fullname string + ProfileImageID sql.NullInt64 + FbProviderType sql.NullString + FbUid sql.NullString + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) { + row := q.db.QueryRowContext(ctx, createUser, + arg.Email, + arg.Nickname, + arg.Fullname, + arg.Password, + arg.ProfileImageID, + arg.FbProviderType, + arg.FbUid, + ) + var i CreateUserRow + err := row.Scan( + &i.ID, + &i.Email, + &i.Nickname, + &i.Fullname, + &i.ProfileImageID, + &i.FbProviderType, + &i.FbUid, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteUserByUID = `-- name: DeleteUserByUID :exec +UPDATE + users +SET deleted_at = NOW() +WHERE fb_uid = $1 +` + +func (q *Queries) DeleteUserByUID(ctx context.Context, fbUid sql.NullString) error { + _, err := q.db.ExecContext(ctx, deleteUserByUID, fbUid) + return err +} + +const existsUserByNickname = `-- name: ExistsUserByNickname :one +SELECT CASE + WHEN + EXISTS (SELECT 1 + FROM users + WHERE nickname = $1 + AND deleted_at IS NULL) + THEN TRUE + ELSE FALSE + END +` + +func (q *Queries) ExistsUserByNickname(ctx context.Context, nickname string) (bool, error) { + row := q.db.QueryRowContext(ctx, existsUserByNickname, nickname) + var column_1 bool + err := row.Scan(&column_1) + return column_1, err +} + +const findUserByEmail = `-- name: FindUserByEmail :one +SELECT users.id, + users.email, + users.nickname, + users.fullname, + media.url AS profile_image_url, + users.fb_provider_type, + users.fb_uid, + users.created_at, + users.updated_at +FROM users + LEFT OUTER JOIN + media + ON + users.profile_image_id = media.id +WHERE users.email = $1 + AND users.deleted_at IS NULL +` + +type FindUserByEmailRow struct { + ID int32 + Email string + Nickname string + Fullname string + ProfileImageUrl sql.NullString + FbProviderType sql.NullString + FbUid sql.NullString + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) FindUserByEmail(ctx context.Context, email string) (FindUserByEmailRow, error) { + row := q.db.QueryRowContext(ctx, findUserByEmail, email) + var i FindUserByEmailRow + err := row.Scan( + &i.ID, + &i.Email, + &i.Nickname, + &i.Fullname, + &i.ProfileImageUrl, + &i.FbProviderType, + &i.FbUid, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const findUserByUID = `-- name: FindUserByUID :one +SELECT users.id, + users.email, + users.nickname, + users.fullname, + media.url AS profile_image_url, + users.fb_provider_type, + users.fb_uid, + users.created_at, + users.updated_at +FROM users + LEFT JOIN + media + ON + users.profile_image_id = media.id +WHERE users.fb_uid = $1 + AND users.deleted_at IS NULL +` + +type FindUserByUIDRow struct { + ID int32 + Email string + Nickname string + Fullname string + ProfileImageUrl sql.NullString + FbProviderType sql.NullString + FbUid sql.NullString + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) FindUserByUID(ctx context.Context, fbUid sql.NullString) (FindUserByUIDRow, error) { + row := q.db.QueryRowContext(ctx, findUserByUID, fbUid) + var i FindUserByUIDRow + err := row.Scan( + &i.ID, + &i.Email, + &i.Nickname, + &i.Fullname, + &i.ProfileImageUrl, + &i.FbProviderType, + &i.FbUid, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const findUserIDByFbUID = `-- name: FindUserIDByFbUID :one +SELECT id +FROM users +WHERE fb_uid = $1 + AND deleted_at IS NULL +` + +func (q *Queries) FindUserIDByFbUID(ctx context.Context, fbUid sql.NullString) (int32, error) { + row := q.db.QueryRowContext(ctx, findUserIDByFbUID, fbUid) + var id int32 + err := row.Scan(&id) + return id, err +} + +const findUserStatusByEmail = `-- name: FindUserStatusByEmail :one +SELECT fb_provider_type +FROM users +WHERE email = $1 + AND deleted_at IS NULL +` + +func (q *Queries) FindUserStatusByEmail(ctx context.Context, email string) (sql.NullString, error) { + row := q.db.QueryRowContext(ctx, findUserStatusByEmail, email) + var fb_provider_type sql.NullString + err := row.Scan(&fb_provider_type) + return fb_provider_type, err +} + +const findUsers = `-- name: FindUsers :many +SELECT users.id, + users.nickname, + media.url AS profile_image_url +FROM users + LEFT OUTER JOIN + media + ON + users.profile_image_id = media.id +WHERE (users.nickname = $1 OR $1 IS NULL) + AND users.deleted_at IS NULL +ORDER BY users.created_at DESC +LIMIT $2 OFFSET $3 +` + +type FindUsersParams struct { + Nickname string + Limit int32 + Offset int32 +} + +type FindUsersRow struct { + ID int32 + Nickname string + ProfileImageUrl sql.NullString +} + +func (q *Queries) FindUsers(ctx context.Context, arg FindUsersParams) ([]FindUsersRow, error) { + rows, err := q.db.QueryContext(ctx, findUsers, arg.Nickname, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindUsersRow + for rows.Next() { + var i FindUsersRow + if err := rows.Scan(&i.ID, &i.Nickname, &i.ProfileImageUrl); 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 +} + +const findUsersByID = `-- name: FindUsersByID :one +SELECT users.id, + users.email, + users.nickname, + users.fullname, + media.url AS profile_image_url, + users.fb_provider_type, + users.fb_uid, + users.created_at, + users.updated_at, + users.deleted_at +FROM users + LEFT OUTER JOIN + media + ON + users.profile_image_id = media.id +WHERE users.id = $1 + AND (users.deleted_at IS NULL OR $2) +` + +type FindUsersByIDParams struct { + ID int32 + Column2 interface{} +} + +type FindUsersByIDRow struct { + ID int32 + Email string + Nickname string + Fullname string + ProfileImageUrl sql.NullString + FbProviderType sql.NullString + FbUid sql.NullString + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +func (q *Queries) FindUsersByID(ctx context.Context, arg FindUsersByIDParams) (FindUsersByIDRow, error) { + row := q.db.QueryRowContext(ctx, findUsersByID, arg.ID, arg.Column2) + var i FindUsersByIDRow + err := row.Scan( + &i.ID, + &i.Email, + &i.Nickname, + &i.Fullname, + &i.ProfileImageUrl, + &i.FbProviderType, + &i.FbUid, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const updateUserByUID = `-- name: UpdateUserByUID :one +UPDATE + users +SET nickname = $1, + profile_image_id = $2, + updated_at = NOW() +WHERE fb_uid = $3 + AND deleted_at IS NULL +RETURNING + id, + email, + nickname, + fullname, + profile_image_id, + fb_provider_type, + fb_uid, + created_at, + updated_at +` + +type UpdateUserByUIDParams struct { + Nickname string + ProfileImageID sql.NullInt64 + FbUid sql.NullString +} + +type UpdateUserByUIDRow struct { + ID int32 + Email string + Nickname string + Fullname string + ProfileImageID sql.NullInt64 + FbProviderType sql.NullString + FbUid sql.NullString + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) UpdateUserByUID(ctx context.Context, arg UpdateUserByUIDParams) (UpdateUserByUIDRow, error) { + row := q.db.QueryRowContext(ctx, updateUserByUID, arg.Nickname, arg.ProfileImageID, arg.FbUid) + var i UpdateUserByUIDRow + err := row.Scan( + &i.ID, + &i.Email, + &i.Nickname, + &i.Fullname, + &i.ProfileImageID, + &i.FbProviderType, + &i.FbUid, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/internal/infra/database/transaction.go b/internal/infra/database/transaction.go index 2079a4a2..0068f51a 100644 --- a/internal/infra/database/transaction.go +++ b/internal/infra/database/transaction.go @@ -10,13 +10,6 @@ import ( "github.com/rs/zerolog/log" ) -type DBTx interface { - ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) - QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) - QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row - PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) -} - type Transactioner interface { Rollback() *pnd.AppError Commit() *pnd.AppError @@ -93,3 +86,19 @@ func WithTransaction(ctx context.Context, conn *DB, f func(tx *Tx) *pnd.AppError return tx.Commit() } + +func (tx *Tx) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + return tx.Tx.ExecContext(ctx, query, args...) +} + +func (tx *Tx) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + return tx.Tx.QueryContext(ctx, query, args...) +} + +func (tx *Tx) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { + return tx.Tx.QueryRowContext(ctx, query, args...) +} + +func (tx *Tx) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { + return tx.Tx.PrepareContext(ctx, query) +} diff --git a/queries/users.sql b/queries/users.sql new file mode 100644 index 00000000..86bc85cc --- /dev/null +++ b/queries/users.sql @@ -0,0 +1,130 @@ +-- name: CreateUser :one +INSERT INTO users +(email, + nickname, + fullname, + password, + profile_image_id, + fb_provider_type, + fb_uid, + created_at, + updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) +RETURNING id, email, nickname, fullname, profile_image_id, fb_provider_type, fb_uid, created_at, updated_at; + +-- name: FindUsers :many +SELECT users.id, + users.nickname, + media.url AS profile_image_url +FROM users + LEFT OUTER JOIN + media + ON + users.profile_image_id = media.id +WHERE (users.nickname = $1 OR $1 IS NULL) + AND users.deleted_at IS NULL +ORDER BY users.created_at DESC +LIMIT $2 OFFSET $3; + +-- name: FindUsersByID :one +SELECT users.id, + users.email, + users.nickname, + users.fullname, + media.url AS profile_image_url, + users.fb_provider_type, + users.fb_uid, + users.created_at, + users.updated_at, + users.deleted_at +FROM users + LEFT OUTER JOIN + media + ON + users.profile_image_id = media.id +WHERE users.id = $1 + AND (users.deleted_at IS NULL OR $2); + +-- name: FindUserByEmail :one +SELECT users.id, + users.email, + users.nickname, + users.fullname, + media.url AS profile_image_url, + users.fb_provider_type, + users.fb_uid, + users.created_at, + users.updated_at +FROM users + LEFT OUTER JOIN + media + ON + users.profile_image_id = media.id +WHERE users.email = $1 + AND users.deleted_at IS NULL; + +-- name: FindUserByUID :one +SELECT users.id, + users.email, + users.nickname, + users.fullname, + media.url AS profile_image_url, + users.fb_provider_type, + users.fb_uid, + users.created_at, + users.updated_at +FROM users + LEFT JOIN + media + ON + users.profile_image_id = media.id +WHERE users.fb_uid = $1 + AND users.deleted_at IS NULL; + +-- name: FindUserIDByFbUID :one +SELECT id +FROM users +WHERE fb_uid = $1 + AND deleted_at IS NULL; + +-- name: ExistsUserByNickname :one +SELECT CASE + WHEN + EXISTS (SELECT 1 + FROM users + WHERE nickname = $1 + AND deleted_at IS NULL) + THEN TRUE + ELSE FALSE + END; + +-- name: FindUserStatusByEmail :one +SELECT fb_provider_type +FROM users +WHERE email = $1 + AND deleted_at IS NULL; + +-- name: UpdateUserByUID :one +UPDATE + users +SET nickname = $1, + profile_image_id = $2, + updated_at = NOW() +WHERE fb_uid = $3 + AND deleted_at IS NULL +RETURNING + id, + email, + nickname, + fullname, + profile_image_id, + fb_provider_type, + fb_uid, + created_at, + updated_at; + +-- name: DeleteUserByUID :exec +UPDATE + users +SET deleted_at = NOW() +WHERE fb_uid = $1; diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 00000000..e1d76bbf --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "queries/*.sql" + schema: "db/migrations" + gen: + go: + package: "databasegen" + out: "internal/infra/database/gen" + sql_package: "database/sql" From c79f895f20375fbe078d401e3901d5b012acd8de Mon Sep 17 00:00:00 2001 From: litsynp Date: Wed, 24 Apr 2024 01:36:42 +0900 Subject: [PATCH 03/12] refactor: use sqlc instead of user store --- internal/common/null.go | 71 +++++ internal/common/pagination.go | 22 ++ internal/domain/user/user.go | 90 +----- internal/domain/user/view.go | 10 +- internal/infra/database/database.go | 1 + internal/infra/database/gen/users.sql.go | 211 ++++--------- internal/infra/database/transaction.go | 20 +- internal/postgres/breed_store.go | 2 +- internal/postgres/media_store.go | 2 +- internal/postgres/pet_store.go | 2 +- internal/postgres/resource_media_store.go | 2 +- internal/postgres/sos_post_store.go | 8 +- internal/postgres/user_store.go | 357 ---------------------- internal/service/sos_post_service.go | 67 ++-- internal/service/user_service.go | 264 ++++++++++------ queries/users.sql | 70 +---- 16 files changed, 421 insertions(+), 778 deletions(-) create mode 100644 internal/common/null.go create mode 100644 internal/common/pagination.go delete mode 100644 internal/postgres/user_store.go diff --git a/internal/common/null.go b/internal/common/null.go new file mode 100644 index 00000000..f763d22e --- /dev/null +++ b/internal/common/null.go @@ -0,0 +1,71 @@ +package utils + +import "database/sql" + +func DerefOrEmpty[T any](val *T) T { + if val == nil { + var empty T + return empty + } + return *val +} + +func IsNotNil[T any](val *T) bool { + return val != nil +} + +func NullStrToStrPtr(val sql.NullString) *string { + if val.Valid { + return &val.String + } + return nil +} + +func StrPtrToNullStr(val *string) sql.NullString { + return sql.NullString{ + String: DerefOrEmpty(val), + Valid: IsNotNil(val), + } +} + +func StrToNullStr(val string) sql.NullString { + return sql.NullString{ + String: val, + Valid: val != "", + } +} + +func NullInt64ToInt64Ptr(val sql.NullInt64) *int64 { + if val.Valid { + return &val.Int64 + } + return nil +} + +func Int64PtrToNullInt64(val *int64) sql.NullInt64 { + return sql.NullInt64{ + Int64: DerefOrEmpty(val), + Valid: IsNotNil(val), + } +} + +func IntPtrToNullInt64(val *int) sql.NullInt64 { + return sql.NullInt64{ + Int64: int64(DerefOrEmpty(val)), + Valid: IsNotNil(val), + } +} + +func IntToNullInt32(val int) sql.NullInt32 { + return sql.NullInt32{ + Int32: int32(val), + Valid: val != 0, + } +} + +func IntPtrToNullInt32(val *int) sql.NullInt32 { + return sql.NullInt32{ + Int32: int32(DerefOrEmpty(val)), + Valid: IsNotNil(val), + } +} diff --git a/internal/common/pagination.go b/internal/common/pagination.go new file mode 100644 index 00000000..60d9e85a --- /dev/null +++ b/internal/common/pagination.go @@ -0,0 +1,22 @@ +package utils + +type PaginationClauses struct { + Offset int + Limit int +} + +func OffsetAndLimit(page, size int) PaginationClauses { + if page < 1 { + page = 1 + } + + if size < 1 { + size = 10 + } + + offset := (page - 1) * size + return PaginationClauses{ + Offset: offset, + Limit: size, + } +} diff --git a/internal/domain/user/user.go b/internal/domain/user/user.go index 6d5626cc..29de8128 100644 --- a/internal/domain/user/user.go +++ b/internal/domain/user/user.go @@ -1,12 +1,9 @@ package user import ( - "context" "database/sql" "time" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - pnd "github.com/pet-sitter/pets-next-door-api/api" ) @@ -19,64 +16,26 @@ const ( FirebaseProviderTypeKakao FirebaseProviderType = "kakao" ) -type User struct { - ID int `field:"id"` - Email string `field:"email"` - Password string `field:"password"` - Nickname string `field:"nickname"` - Fullname string `field:"fullname"` - ProfileImageID *int `field:"profile_image_id"` - FirebaseProviderType FirebaseProviderType `field:"fb_provider_type"` - FirebaseUID string `field:"fb_uid"` - CreatedAt time.Time `field:"created_at"` - UpdatedAt time.Time `field:"updated_at"` - DeletedAt sql.NullTime `field:"deleted_at"` +func (f FirebaseProviderType) String() string { + return string(f) } -func (u *User) ToUserWithProfileImage(profileImageURL *string) *UserWithProfileImage { - return &UserWithProfileImage{ - ID: u.ID, - Email: u.Email, - Password: u.Password, - Nickname: u.Nickname, - Fullname: u.Fullname, - ProfileImageURL: profileImageURL, - FirebaseProviderType: u.FirebaseProviderType, - FirebaseUID: u.FirebaseUID, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, - DeletedAt: u.DeletedAt, - } +func (f FirebaseProviderType) NullString() sql.NullString { + return sql.NullString{String: string(f), Valid: true} } type UserWithProfileImage struct { - ID int `field:"id"` - Email string `field:"email"` - Password string `field:"password"` - Nickname string `field:"nickname"` - Fullname string `field:"fullname"` - ProfileImageURL *string `field:"profile_image_url"` - FirebaseProviderType FirebaseProviderType `field:"fb_provider_type"` - FirebaseUID string `field:"fb_uid"` - CreatedAt time.Time `field:"created_at"` - UpdatedAt time.Time `field:"updated_at"` - DeletedAt sql.NullTime `field:"deleted_at"` -} - -func (u *UserWithProfileImage) ToUserWithoutPrivateInfo() *UserWithoutPrivateInfo { - if u.DeletedAt.Valid { - return &UserWithoutPrivateInfo{ - ID: u.ID, - Nickname: "탈퇴한 사용자", - ProfileImageURL: nil, - } - } - - return &UserWithoutPrivateInfo{ - ID: u.ID, - Nickname: u.Nickname, - ProfileImageURL: u.ProfileImageURL, - } + ID int + Email string + Password string + Nickname string + Fullname string + ProfileImageURL *string + FirebaseProviderType FirebaseProviderType + FirebaseUID string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime } type UserWithoutPrivateInfo struct { @@ -98,22 +57,3 @@ func NewUserWithoutPrivateInfoList(page, size int) *UserWithoutPrivateInfoList { type UserStatus struct { FirebaseProviderType FirebaseProviderType `field:"fb_provider_type"` } - -type UserStore interface { - CreateUser(ctx context.Context, tx *database.Tx, request *RegisterUserRequest) (*User, *pnd.AppError) - FindUsers( - ctx context.Context, - tx *database.Tx, - page int, - size int, - nickname *string, - ) (*UserWithoutPrivateInfoList, *pnd.AppError) - FindUserByID(ctx context.Context, tx *database.Tx, id int, includeDeleted bool) (*UserWithProfileImage, *pnd.AppError) - FindUserByEmail(ctx context.Context, tx *database.Tx, email string) (*UserWithProfileImage, *pnd.AppError) - FindUserByUID(ctx context.Context, tx *database.Tx, uid string) (*UserWithProfileImage, *pnd.AppError) - FindUserIDByFbUID(ctx context.Context, tx *database.Tx, fbUID string) (int, *pnd.AppError) - ExistsUserByNickname(ctx context.Context, tx *database.Tx, nickname string) (bool, *pnd.AppError) - FindUserStatusByEmail(ctx context.Context, tx *database.Tx, email string) (*UserStatus, *pnd.AppError) - UpdateUserByUID(ctx context.Context, tx *database.Tx, uid, nickname string, profileImageID *int) (*User, *pnd.AppError) - DeleteUserByUID(ctx context.Context, tx *database.Tx, uid string) *pnd.AppError -} diff --git a/internal/domain/user/view.go b/internal/domain/user/view.go index caac4e79..5f6c2d45 100644 --- a/internal/domain/user/view.go +++ b/internal/domain/user/view.go @@ -1,5 +1,7 @@ package user +import databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" + type RegisterUserRequest struct { Email string `json:"email" validate:"required,email"` Nickname string `json:"nickname" validate:"required"` @@ -19,15 +21,15 @@ type RegisterUserView struct { FirebaseUID string `json:"fbUid"` } -func (u *User) ToRegisterUserView(profileImageURL *string) *RegisterUserView { +func ToRegisterUserView(u *databasegen.CreateUserRow, profileImageURL *string) *RegisterUserView { return &RegisterUserView{ - ID: u.ID, + ID: int(u.ID), Email: u.Email, Nickname: u.Nickname, Fullname: u.Fullname, ProfileImageURL: profileImageURL, - FirebaseProviderType: u.FirebaseProviderType, - FirebaseUID: u.FirebaseUID, + FirebaseProviderType: FirebaseProviderType(u.FbProviderType.String), + FirebaseUID: u.FbUid.String, } } diff --git a/internal/infra/database/database.go b/internal/infra/database/database.go index 0ffb1e79..d306a41d 100644 --- a/internal/infra/database/database.go +++ b/internal/infra/database/database.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" "github.com/golang-migrate/migrate/v4" diff --git a/internal/infra/database/gen/users.sql.go b/internal/infra/database/gen/users.sql.go index 5e145413..b8da3b74 100644 --- a/internal/infra/database/gen/users.sql.go +++ b/internal/infra/database/gen/users.sql.go @@ -73,15 +73,15 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU return i, err } -const deleteUserByUID = `-- name: DeleteUserByUID :exec +const deleteUserByFbUID = `-- name: DeleteUserByFbUID :exec UPDATE users SET deleted_at = NOW() WHERE fb_uid = $1 ` -func (q *Queries) DeleteUserByUID(ctx context.Context, fbUid sql.NullString) error { - _, err := q.db.ExecContext(ctx, deleteUserByUID, fbUid) +func (q *Queries) DeleteUserByFbUID(ctx context.Context, fbUid sql.NullString) error { + _, err := q.db.ExecContext(ctx, deleteUserByFbUID, fbUid) return err } @@ -104,7 +104,7 @@ func (q *Queries) ExistsUserByNickname(ctx context.Context, nickname string) (bo return column_1, err } -const findUserByEmail = `-- name: FindUserByEmail :one +const findUser = `-- name: FindUser :one SELECT users.id, users.email, users.nickname, @@ -113,65 +113,29 @@ SELECT users.id, users.fb_provider_type, users.fb_uid, users.created_at, - users.updated_at + users.updated_at, + users.deleted_at FROM users LEFT OUTER JOIN media ON users.profile_image_id = media.id -WHERE users.email = $1 - AND users.deleted_at IS NULL +WHERE (users.id = $1 OR $1 IS NULL) + AND (users.nickname = $2 OR $2 IS NULL) + AND (users.email = $3 OR $3 IS NULL) + AND (users.fb_uid = $4 OR $4 IS NULL) + AND (users.deleted_at IS NULL OR $5::boolean = TRUE) ` -type FindUserByEmailRow struct { - ID int32 - Email string - Nickname string - Fullname string - ProfileImageUrl sql.NullString - FbProviderType sql.NullString - FbUid sql.NullString - CreatedAt time.Time - UpdatedAt time.Time -} - -func (q *Queries) FindUserByEmail(ctx context.Context, email string) (FindUserByEmailRow, error) { - row := q.db.QueryRowContext(ctx, findUserByEmail, email) - var i FindUserByEmailRow - err := row.Scan( - &i.ID, - &i.Email, - &i.Nickname, - &i.Fullname, - &i.ProfileImageUrl, - &i.FbProviderType, - &i.FbUid, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err +type FindUserParams struct { + ID sql.NullInt32 + Nickname sql.NullString + Email sql.NullString + FbUid sql.NullString + IncludeDeleted bool } -const findUserByUID = `-- name: FindUserByUID :one -SELECT users.id, - users.email, - users.nickname, - users.fullname, - media.url AS profile_image_url, - users.fb_provider_type, - users.fb_uid, - users.created_at, - users.updated_at -FROM users - LEFT JOIN - media - ON - users.profile_image_id = media.id -WHERE users.fb_uid = $1 - AND users.deleted_at IS NULL -` - -type FindUserByUIDRow struct { +type FindUserRow struct { ID int32 Email string Nickname string @@ -181,11 +145,18 @@ type FindUserByUIDRow struct { FbUid sql.NullString CreatedAt time.Time UpdatedAt time.Time + DeletedAt sql.NullTime } -func (q *Queries) FindUserByUID(ctx context.Context, fbUid sql.NullString) (FindUserByUIDRow, error) { - row := q.db.QueryRowContext(ctx, findUserByUID, fbUid) - var i FindUserByUIDRow +func (q *Queries) FindUser(ctx context.Context, arg FindUserParams) (FindUserRow, error) { + row := q.db.QueryRowContext(ctx, findUser, + arg.ID, + arg.Nickname, + arg.Email, + arg.FbUid, + arg.IncludeDeleted, + ) + var i FindUserRow err := row.Scan( &i.ID, &i.Email, @@ -196,38 +167,11 @@ func (q *Queries) FindUserByUID(ctx context.Context, fbUid sql.NullString) (Find &i.FbUid, &i.CreatedAt, &i.UpdatedAt, + &i.DeletedAt, ) return i, err } -const findUserIDByFbUID = `-- name: FindUserIDByFbUID :one -SELECT id -FROM users -WHERE fb_uid = $1 - AND deleted_at IS NULL -` - -func (q *Queries) FindUserIDByFbUID(ctx context.Context, fbUid sql.NullString) (int32, error) { - row := q.db.QueryRowContext(ctx, findUserIDByFbUID, fbUid) - var id int32 - err := row.Scan(&id) - return id, err -} - -const findUserStatusByEmail = `-- name: FindUserStatusByEmail :one -SELECT fb_provider_type -FROM users -WHERE email = $1 - AND deleted_at IS NULL -` - -func (q *Queries) FindUserStatusByEmail(ctx context.Context, email string) (sql.NullString, error) { - row := q.db.QueryRowContext(ctx, findUserStatusByEmail, email) - var fb_provider_type sql.NullString - err := row.Scan(&fb_provider_type) - return fb_provider_type, err -} - const findUsers = `-- name: FindUsers :many SELECT users.id, users.nickname, @@ -237,16 +181,23 @@ FROM users media ON users.profile_image_id = media.id -WHERE (users.nickname = $1 OR $1 IS NULL) - AND users.deleted_at IS NULL +WHERE (users.id = $3 OR $3 IS NULL) + AND (users.nickname = $4 OR $4 IS NULL) + AND (users.email = $5 OR $5 IS NULL) + AND (users.fb_uid = $6 OR $6 IS NULL) + AND (users.deleted_at IS NULL OR $7::boolean = TRUE) ORDER BY users.created_at DESC -LIMIT $2 OFFSET $3 +LIMIT $1 OFFSET $2 ` type FindUsersParams struct { - Nickname string - Limit int32 - Offset int32 + Limit int32 + Offset int32 + ID sql.NullInt32 + Nickname sql.NullString + Email sql.NullString + FbUid sql.NullString + IncludeDeleted bool } type FindUsersRow struct { @@ -256,7 +207,15 @@ type FindUsersRow struct { } func (q *Queries) FindUsers(ctx context.Context, arg FindUsersParams) ([]FindUsersRow, error) { - rows, err := q.db.QueryContext(ctx, findUsers, arg.Nickname, arg.Limit, arg.Offset) + rows, err := q.db.QueryContext(ctx, findUsers, + arg.Limit, + arg.Offset, + arg.ID, + arg.Nickname, + arg.Email, + arg.FbUid, + arg.IncludeDeleted, + ) if err != nil { return nil, err } @@ -278,63 +237,7 @@ func (q *Queries) FindUsers(ctx context.Context, arg FindUsersParams) ([]FindUse return items, nil } -const findUsersByID = `-- name: FindUsersByID :one -SELECT users.id, - users.email, - users.nickname, - users.fullname, - media.url AS profile_image_url, - users.fb_provider_type, - users.fb_uid, - users.created_at, - users.updated_at, - users.deleted_at -FROM users - LEFT OUTER JOIN - media - ON - users.profile_image_id = media.id -WHERE users.id = $1 - AND (users.deleted_at IS NULL OR $2) -` - -type FindUsersByIDParams struct { - ID int32 - Column2 interface{} -} - -type FindUsersByIDRow struct { - ID int32 - Email string - Nickname string - Fullname string - ProfileImageUrl sql.NullString - FbProviderType sql.NullString - FbUid sql.NullString - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt sql.NullTime -} - -func (q *Queries) FindUsersByID(ctx context.Context, arg FindUsersByIDParams) (FindUsersByIDRow, error) { - row := q.db.QueryRowContext(ctx, findUsersByID, arg.ID, arg.Column2) - var i FindUsersByIDRow - err := row.Scan( - &i.ID, - &i.Email, - &i.Nickname, - &i.Fullname, - &i.ProfileImageUrl, - &i.FbProviderType, - &i.FbUid, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ) - return i, err -} - -const updateUserByUID = `-- name: UpdateUserByUID :one +const updateUserByFbUID = `-- name: UpdateUserByFbUID :one UPDATE users SET nickname = $1, @@ -354,13 +257,13 @@ RETURNING updated_at ` -type UpdateUserByUIDParams struct { +type UpdateUserByFbUIDParams struct { Nickname string ProfileImageID sql.NullInt64 FbUid sql.NullString } -type UpdateUserByUIDRow struct { +type UpdateUserByFbUIDRow struct { ID int32 Email string Nickname string @@ -372,9 +275,9 @@ type UpdateUserByUIDRow struct { UpdatedAt time.Time } -func (q *Queries) UpdateUserByUID(ctx context.Context, arg UpdateUserByUIDParams) (UpdateUserByUIDRow, error) { - row := q.db.QueryRowContext(ctx, updateUserByUID, arg.Nickname, arg.ProfileImageID, arg.FbUid) - var i UpdateUserByUIDRow +func (q *Queries) UpdateUserByFbUID(ctx context.Context, arg UpdateUserByFbUIDParams) (UpdateUserByFbUIDRow, error) { + row := q.db.QueryRowContext(ctx, updateUserByFbUID, arg.Nickname, arg.ProfileImageID, arg.FbUid) + var i UpdateUserByFbUIDRow err := row.Scan( &i.ID, &i.Email, diff --git a/internal/infra/database/transaction.go b/internal/infra/database/transaction.go index 0068f51a..bd41cec5 100644 --- a/internal/infra/database/transaction.go +++ b/internal/infra/database/transaction.go @@ -26,22 +26,20 @@ func (db *DB) BeginTx(ctx context.Context) (*Tx, *pnd.AppError) { return &Tx{tx}, nil } -func (sct *Tx) EndTx(f func() *pnd.AppError) *pnd.AppError { +func (tx *Tx) EndTx(f func() *pnd.AppError) *pnd.AppError { var err *pnd.AppError - tx := sct.Tx - defer func() { if p := recover(); p != nil { - if err2 := tx.Rollback(); err2 != nil { + if err2 := tx.Tx.Rollback(); err2 != nil { log.Error().Err(err2).Msg("error rolling back transaction") panic(p) } } else if err = f(); err != nil { - if err2 := tx.Rollback(); err2 != nil { + if err2 := tx.Tx.Rollback(); err2 != nil { log.Error().Err(err2).Msg("error rolling back transaction") } - } else if err := tx.Commit(); err != nil { - if err2 := tx.Rollback(); err2 != nil { + } else if err := tx.Tx.Commit(); err != nil { + if err2 := tx.Tx.Rollback(); err2 != nil { log.Error().Err(err2).Msg("error rolling back transaction") } } @@ -51,8 +49,8 @@ func (sct *Tx) EndTx(f func() *pnd.AppError) *pnd.AppError { return err } -func (sct *Tx) Rollback() *pnd.AppError { - if err := sct.Tx.Rollback(); err != nil { +func (tx *Tx) Rollback() *pnd.AppError { + if err := tx.Tx.Rollback(); err != nil { if errors.Is(err, sql.ErrTxDone) { return nil } @@ -62,8 +60,8 @@ func (sct *Tx) Rollback() *pnd.AppError { return nil } -func (sct *Tx) Commit() *pnd.AppError { - if err := sct.Tx.Commit(); err != nil { +func (tx *Tx) Commit() *pnd.AppError { + if err := tx.Tx.Commit(); err != nil { return pnd.FromPostgresError(err) } diff --git a/internal/postgres/breed_store.go b/internal/postgres/breed_store.go index 758babac..5bcb2b44 100644 --- a/internal/postgres/breed_store.go +++ b/internal/postgres/breed_store.go @@ -100,7 +100,7 @@ func CreateBreed(ctx context.Context, tx *database.Tx, breed *pet.Breed) (*pet.B id, pet_type, name, created_at, updated_at ` - if err := tx.QueryRowContext(ctx, sql, //nolint:execinquery + if err := tx.QueryRowContext(ctx, sql, breed.Name, breed.PetType, ).Scan( diff --git a/internal/postgres/media_store.go b/internal/postgres/media_store.go index 5dcde7a5..4ee4b289 100644 --- a/internal/postgres/media_store.go +++ b/internal/postgres/media_store.go @@ -22,7 +22,7 @@ func CreateMedia(ctx context.Context, tx *database.Tx, mediaData *media.Media) ( RETURNING id, created_at, updated_at ` - if err := tx.QueryRowContext(ctx, sql, //nolint:execinquery + if err := tx.QueryRowContext(ctx, sql, mediaData.MediaType, mediaData.URL, ).Scan(&mediaData.ID, &mediaData.CreatedAt, &mediaData.UpdatedAt); err != nil { diff --git a/internal/postgres/pet_store.go b/internal/postgres/pet_store.go index dbc6ad7e..e4846334 100644 --- a/internal/postgres/pet_store.go +++ b/internal/postgres/pet_store.go @@ -31,7 +31,7 @@ func CreatePet(ctx context.Context, tx *database.Tx, petData *pet.Pet) (*pet.Pet RETURNING id, created_at, updated_at ` - if err := tx.QueryRowContext(ctx, sql, //nolint:execinquery + if err := tx.QueryRowContext(ctx, sql, petData.OwnerID, petData.Name, petData.PetType, diff --git a/internal/postgres/resource_media_store.go b/internal/postgres/resource_media_store.go index b9486b75..0e9ea271 100644 --- a/internal/postgres/resource_media_store.go +++ b/internal/postgres/resource_media_store.go @@ -26,7 +26,7 @@ func CreateResourceMedia( ` resourceMedia := &media.ResourceMedia{} - err := tx.QueryRowContext(ctx, sql, //nolint:execinquery + err := tx.QueryRowContext(ctx, sql, resourceID, mediaID, resourceType, diff --git a/internal/postgres/sos_post_store.go b/internal/postgres/sos_post_store.go index 68b99a65..377855cb 100644 --- a/internal/postgres/sos_post_store.go +++ b/internal/postgres/sos_post_store.go @@ -48,7 +48,7 @@ func WriteSOSPost( ctx context.Context, tx *database.Tx, authorID int, request *sospost.WriteSOSPostRequest, ) (*sospost.SOSPost, *pnd.AppError) { sosPost := &sospost.SOSPost{} - err := tx.QueryRowContext(ctx, writeSOSPostQuery, //nolint:execinquery + err := tx.QueryRowContext(ctx, writeSOSPostQuery, authorID, request.Title, request.Content, @@ -87,7 +87,7 @@ func WriteSOSPost( for _, date := range request.Dates { sosDate := sospost.SOSDates{} - if err := tx.QueryRowContext(ctx, sql2, //nolint:execinquery + if err := tx.QueryRowContext(ctx, sql2, date.DateStartAt, date.DateEndAt, ).Scan( @@ -501,7 +501,7 @@ func UpdateSOSPost( thumbnail_id ` - if err := tx.QueryRowContext(ctx, query, //nolint:execinquery + if err := tx.QueryRowContext(ctx, query, request.Title, request.Content, request.Reward, @@ -554,7 +554,7 @@ func updateSOSPostsDates(ctx context.Context, tx *database.Tx, postID int, dates sosDates := []sospost.SOSDates{} for _, date := range dates { sosDate := sospost.SOSDates{} - if err := tx.QueryRowContext(ctx, query, //nolint:execinquery + if err := tx.QueryRowContext(ctx, query, date.DateStartAt, date.DateEndAt, ).Scan( diff --git a/internal/postgres/user_store.go b/internal/postgres/user_store.go deleted file mode 100644 index b43028b4..00000000 --- a/internal/postgres/user_store.go +++ /dev/null @@ -1,357 +0,0 @@ -package postgres - -import ( - "context" - - pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/domain/user" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" -) - -func CreateUser(ctx context.Context, tx *database.Tx, request *user.RegisterUserRequest) (*user.User, *pnd.AppError) { - const sql = ` - INSERT INTO - users - ( - email, - nickname, - fullname, - password, - profile_image_id, - fb_provider_type, - fb_uid, - created_at, - updated_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) - RETURNING id, email, nickname, fullname, profile_image_id, fb_provider_type, fb_uid, created_at, updated_at - ` - - userData := &user.User{} - if err := tx.QueryRowContext(ctx, sql, //nolint:execinquery - request.Email, - request.Nickname, - request.Fullname, - "", - request.ProfileImageID, - request.FirebaseProviderType, - request.FirebaseUID, - ).Scan( - &userData.ID, - &userData.Email, - &userData.Nickname, - &userData.Fullname, - &userData.ProfileImageID, - &userData.FirebaseProviderType, - &userData.FirebaseUID, - &userData.CreatedAt, - &userData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return userData, nil -} - -func FindUsers( - ctx context.Context, tx *database.Tx, page, size int, nickname *string, -) (*user.UserWithoutPrivateInfoList, *pnd.AppError) { - const sql = ` - SELECT - users.id, - users.nickname, - media.url AS profile_image_url - FROM - users - LEFT OUTER JOIN - media - ON - users.profile_image_id = media.id - WHERE - (users.nickname = $1 OR $1 IS NULL) AND - users.deleted_at IS NULL - ORDER BY - users.created_at DESC - LIMIT $2 - OFFSET $3 - ` - - userList := user.NewUserWithoutPrivateInfoList(page, size) - rows, err := tx.QueryContext(ctx, sql, nickname, size+1, (page-1)*size) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - defer rows.Close() - - for rows.Next() { - userData := &user.UserWithoutPrivateInfo{} - - err := rows.Scan(&userData.ID, &userData.Nickname, &userData.ProfileImageURL) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - - userList.Items = append(userList.Items, *userData) - } - if err := rows.Err(); err != nil { - return nil, pnd.FromPostgresError(err) - } - - userList.CalcLastPage() - return userList, nil -} - -func FindUserByID( - ctx context.Context, tx *database.Tx, id int, includeDeleted bool, -) (*user.UserWithProfileImage, *pnd.AppError) { - const sql = ` - SELECT - users.id, - users.email, - users.nickname, - users.fullname, - media.url AS profile_image_url, - users.fb_provider_type, - users.fb_uid, - users.created_at, - users.updated_at, - users.deleted_at - FROM - users - LEFT OUTER JOIN - media - ON - users.profile_image_id = media.id - WHERE - users.id = $1 AND - (users.deleted_at IS NULL OR $2) - ` - - var userData user.UserWithProfileImage - if err := tx.QueryRowContext(ctx, sql, id, includeDeleted).Scan( - &userData.ID, - &userData.Email, - &userData.Nickname, - &userData.Fullname, - &userData.ProfileImageURL, - &userData.FirebaseProviderType, - &userData.FirebaseUID, - &userData.CreatedAt, - &userData.UpdatedAt, - &userData.DeletedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return &userData, nil -} - -func FindUserByEmail(ctx context.Context, tx *database.Tx, email string) (*user.UserWithProfileImage, *pnd.AppError) { - const sql = ` - SELECT - users.id, - users.email, - users.nickname, - users.fullname, - media.url AS profile_image_url, - users.fb_provider_type, - users.fb_uid, - users.created_at, - users.updated_at - FROM - users - LEFT OUTER JOIN - media - ON - users.profile_image_id = media.id - WHERE - users.email = $1 AND - users.deleted_at IS NULL - ` - - var userData user.UserWithProfileImage - if err := tx.QueryRowContext(ctx, sql, email).Scan( - &userData.ID, - &userData.Email, - &userData.Nickname, - &userData.Fullname, - &userData.ProfileImageURL, - &userData.FirebaseProviderType, - &userData.FirebaseUID, - &userData.CreatedAt, - &userData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return &userData, nil -} - -func FindUserByUID(ctx context.Context, tx *database.Tx, uid string) (*user.UserWithProfileImage, *pnd.AppError) { - const sql = ` - SELECT - users.id, - users.email, - users.nickname, - users.fullname, - media.url AS profile_image_url, - users.fb_provider_type, - users.fb_uid, - users.created_at, - users.updated_at - FROM - users - LEFT JOIN - media - ON - users.profile_image_id = media.id - WHERE - users.fb_uid = $1 AND - users.deleted_at IS NULL - ` - - var userData user.UserWithProfileImage - if err := tx.QueryRowContext(ctx, sql, uid).Scan( - &userData.ID, - &userData.Email, - &userData.Nickname, - &userData.Fullname, - &userData.ProfileImageURL, - &userData.FirebaseProviderType, - &userData.FirebaseUID, - &userData.CreatedAt, - &userData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return &userData, nil -} - -func FindUserIDByFbUID(ctx context.Context, tx *database.Tx, fbUID string) (int, *pnd.AppError) { - const sql = ` - SELECT - id - FROM - users - WHERE - fb_uid = $1 AND - deleted_at IS NULL - ` - - var userID int - if err := tx.QueryRowContext(ctx, sql, fbUID).Scan(&userID); err != nil { - return 0, pnd.FromPostgresError(err) - } - - return userID, nil -} - -func ExistsUserByNickname(ctx context.Context, tx *database.Tx, nickname string) (bool, *pnd.AppError) { - const sql = ` - SELECT - CASE - WHEN EXISTS ( - SELECT - 1 - FROM - users - WHERE - nickname = $1 AND - deleted_at IS NULL - ) THEN TRUE - ELSE FALSE - END - ` - - var exists bool - if err := tx.QueryRowContext(ctx, sql, nickname).Scan(&exists); err != nil { - return false, pnd.FromPostgresError(err) - } - - return exists, nil -} - -func FindUserStatusByEmail(ctx context.Context, tx *database.Tx, email string) (*user.UserStatus, *pnd.AppError) { - const sql = ` - SELECT - fb_provider_type - FROM - users - WHERE - email = $1 AND - deleted_at IS NULL - ` - - var userStatus user.UserStatus - if err := tx.QueryRowContext(ctx, sql, email).Scan(&userStatus.FirebaseProviderType); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return &userStatus, nil -} - -func UpdateUserByUID( - ctx context.Context, tx *database.Tx, uid, nickname string, profileImageID *int, -) (*user.User, *pnd.AppError) { - const sql = ` - UPDATE - users - SET - nickname = $1, - profile_image_id = $2, - updated_at = NOW() - WHERE - fb_uid = $3 AND - deleted_at IS NULL - RETURNING - id, - email, - nickname, - fullname, - profile_image_id, - fb_provider_type, - fb_uid, - created_at, - updated_at - ` - - var userData user.User - err := tx.QueryRowContext(ctx, sql, //nolint:execinquery - nickname, - profileImageID, - uid, - ).Scan( - &userData.ID, - &userData.Email, - &userData.Nickname, - &userData.Fullname, - &userData.ProfileImageID, - &userData.FirebaseProviderType, - &userData.FirebaseUID, - &userData.CreatedAt, - &userData.UpdatedAt, - ) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - - return &userData, nil -} - -func DeleteUserByUID(ctx context.Context, tx *database.Tx, uid string) *pnd.AppError { - const sql = ` - UPDATE - users - SET - deleted_at = NOW() - WHERE - fb_uid = $1 - ` - - if _, err := tx.ExecContext(ctx, sql, uid); err != nil { - return pnd.FromPostgresError(err) - } - - return nil -} diff --git a/internal/service/sos_post_service.go b/internal/service/sos_post_service.go index 825ac542..a2bad61a 100644 --- a/internal/service/sos_post_service.go +++ b/internal/service/sos_post_service.go @@ -3,6 +3,10 @@ package service import ( "context" + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + "github.com/pet-sitter/pets-next-door-api/internal/domain/user" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" + "github.com/pet-sitter/pets-next-door-api/internal/infra/database" "github.com/pet-sitter/pets-next-door-api/internal/postgres" @@ -30,12 +34,14 @@ func (service *SOSPostService) WriteSOSPost( return nil, err } - userID, err := postgres.FindUserIDByFbUID(ctx, tx, fbUID) - if err != nil { - return nil, err + userData, err2 := databasegen.New(tx).FindUser(ctx, databasegen.FindUserParams{ + FbUid: utils.StrToNullStr(fbUID), + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } - sosPost, err := postgres.WriteSOSPost(ctx, tx, userID, request) + sosPost, err := postgres.WriteSOSPost(ctx, tx, int(userData.ID), request) if err != nil { return nil, err } @@ -89,12 +95,20 @@ func (service *SOSPostService) FindSOSPosts( sosPostViews := sospost.FromEmptySOSPostInfoList(sosPosts) for _, sosPost := range sosPosts.Items { - author, err := postgres.FindUserByID(ctx, tx, sosPost.AuthorID, true) + author, err := databasegen.New(tx).FindUser(ctx, databasegen.FindUserParams{ + ID: utils.IntToNullInt32(sosPost.AuthorID), + IncludeDeleted: true, + }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } + sosPostView := sosPost.ToFindSOSPostInfoView( - author.ToUserWithoutPrivateInfo(), + &user.UserWithoutPrivateInfo{ + ID: int(author.ID), + Nickname: author.Nickname, + ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), + }, sosPost.Media.ToMediaViewList(), sosPost.Conditions.ToConditionViewList(), sosPost.Pets.ToPetViewList(), @@ -123,12 +137,20 @@ func (service *SOSPostService) FindSOSPostsByAuthorID( sosPostViews := sospost.FromEmptySOSPostInfoList(sosPosts) for _, sosPost := range sosPosts.Items { - author, err := postgres.FindUserByID(ctx, tx, sosPost.AuthorID, true) + author, err := databasegen.New(tx).FindUser(ctx, databasegen.FindUserParams{ + ID: utils.IntToNullInt32(sosPost.AuthorID), + IncludeDeleted: true, + }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } + sosPostView := sosPost.ToFindSOSPostInfoView( - author.ToUserWithoutPrivateInfo(), + &user.UserWithoutPrivateInfo{ + ID: int(author.ID), + Nickname: author.Nickname, + ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), + }, sosPost.Media.ToMediaViewList(), sosPost.Conditions.ToConditionViewList(), sosPost.Pets.ToPetViewList(), @@ -152,9 +174,12 @@ func (service *SOSPostService) FindSOSPostByID(ctx context.Context, id int) (*so return nil, err } - author, err := postgres.FindUserByID(ctx, tx, sosPost.AuthorID, true) - if err != nil { - return nil, err + author, err2 := databasegen.New(tx).FindUser(ctx, databasegen.FindUserParams{ + ID: utils.IntToNullInt32(sosPost.AuthorID), + IncludeDeleted: true, + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } if err := tx.Commit(); err != nil { @@ -162,7 +187,11 @@ func (service *SOSPostService) FindSOSPostByID(ctx context.Context, id int) (*so } return sosPost.ToFindSOSPostInfoView( - author.ToUserWithoutPrivateInfo(), + &user.UserWithoutPrivateInfo{ + ID: int(author.ID), + Nickname: author.Nickname, + ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), + }, sosPost.Media.ToMediaViewList(), sosPost.Conditions.ToConditionViewList(), sosPost.Pets.ToPetViewList(), @@ -225,9 +254,11 @@ func (service *SOSPostService) CheckUpdatePermission( return false, err } - userID, err := postgres.FindUserIDByFbUID(ctx, tx, fbUID) - if err != nil { - return false, err + userData, err2 := databasegen.New(tx).FindUser(ctx, databasegen.FindUserParams{ + FbUid: utils.StrToNullStr(fbUID), + }) + if err2 != nil { + return false, pnd.FromPostgresError(err2) } sosPost, err := postgres.FindSOSPostByID(ctx, tx, sosPostID) @@ -239,5 +270,5 @@ func (service *SOSPostService) CheckUpdatePermission( return false, err } - return userID == sosPost.AuthorID, nil + return int(userData.ID) == sosPost.AuthorID, nil } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index d3e75921..baa460a2 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -5,6 +5,9 @@ import ( "errors" "fmt" + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" + pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/domain/user" @@ -45,53 +48,108 @@ func (service *UserService) RegisterUser( return nil, err } - created, err := postgres.CreateUser(ctx, tx, registerUserRequest) - if err != nil { - return nil, err + created, err2 := databasegen.New(service.conn).WithTx(tx.Tx).CreateUser(ctx, databasegen.CreateUserParams{ + Email: registerUserRequest.Email, + Nickname: registerUserRequest.Nickname, + Fullname: registerUserRequest.Fullname, + Password: "", + ProfileImageID: utils.IntPtrToNullInt64(registerUserRequest.ProfileImageID), + FbProviderType: registerUserRequest.FirebaseProviderType.NullString(), + FbUid: utils.StrToNullStr(registerUserRequest.FirebaseUID), + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } if err := tx.Commit(); err != nil { return nil, err } - return created.ToRegisterUserView(profileImageURL), nil + return user.ToRegisterUserView(&created, profileImageURL), nil } func (service *UserService) FindUsers( ctx context.Context, page, size int, nickname *string, ) (*user.UserWithoutPrivateInfoList, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() + pagination := utils.OffsetAndLimit(page, size) + rows, err := databasegen.New(service.conn).FindUsers(ctx, databasegen.FindUsersParams{ + Limit: int32(pagination.Limit), + Offset: int32(pagination.Offset), + Nickname: utils.StrPtrToNullStr(nickname), + IncludeDeleted: false, + }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - userList, err := postgres.FindUsers(ctx, tx, page, size, nickname) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err + userList := user.NewUserWithoutPrivateInfoList(page, size) + for _, row := range rows { + userList.Items = append(userList.Items, user.UserWithoutPrivateInfo{ + ID: int(row.ID), + Nickname: row.Nickname, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + }) } + userList.CalcLastPage() return userList, nil } -func (service *UserService) findUserByUID(ctx context.Context, uid string) (*user.UserWithProfileImage, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() +type FindUserParams struct { + ID *int + Email *string + FbUID *string + IncludeDeleted bool +} + +func (service *UserService) FindUser( + ctx context.Context, + params FindUserParams, +) (*user.UserWithProfileImage, *pnd.AppError) { + row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + ID: utils.IntPtrToNullInt32(params.ID), + Email: utils.StrPtrToNullStr(params.Email), + FbUid: utils.StrPtrToNullStr(params.FbUID), + IncludeDeleted: params.IncludeDeleted, + }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - userData, err := postgres.FindUserByUID(ctx, tx, uid) + userData := &user.UserWithProfileImage{ + ID: int(row.ID), + Email: row.Email, + Nickname: row.Nickname, + Fullname: row.Fullname, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), + FirebaseUID: row.FbUid.String, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + } + + return userData, nil +} + +func (service *UserService) findUserByUID(ctx context.Context, uid string) (*user.UserWithProfileImage, *pnd.AppError) { + row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + FbUid: utils.StrToNullStr(uid), + IncludeDeleted: false, + }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - if err := tx.Commit(); err != nil { - return nil, err + userData := &user.UserWithProfileImage{ + ID: int(row.ID), + Email: row.Email, + Nickname: row.Nickname, + Fullname: row.Fullname, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), + FirebaseUID: row.FbUid.String, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, } return userData, nil @@ -102,99 +160,94 @@ func (service *UserService) findUserByUID(ctx context.Context, uid string) (*use func (service *UserService) FindPublicUserByID( ctx context.Context, id int, ) (*user.UserWithoutPrivateInfo, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() + row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + ID: utils.IntToNullInt32(id), + IncludeDeleted: true, + }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - userData, err := postgres.FindUserByID(ctx, tx, id, true) - if err != nil { - return nil, err + userData := &user.UserWithoutPrivateInfo{ + ID: int(row.ID), + Nickname: row.Nickname, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), } - if err := tx.Commit(); err != nil { - return nil, err - } - - return userData.ToUserWithoutPrivateInfo(), nil + return userData, nil } func (service *UserService) FindUserByEmail( ctx context.Context, email string, ) (*user.UserWithProfileImage, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() + row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + Email: utils.StrToNullStr(email), + IncludeDeleted: false, + }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - userData, err := postgres.FindUserByEmail(ctx, tx, email) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err + userData := &user.UserWithProfileImage{ + ID: int(row.ID), + Email: row.Email, + Nickname: row.Nickname, + Fullname: row.Fullname, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), + FirebaseUID: row.FbUid.String, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, } return userData, nil } func (service *UserService) FindUserByUID(ctx context.Context, uid string) (*user.FindUserView, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() + row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + FbUid: utils.StrToNullStr(uid), + IncludeDeleted: false, + }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - foundUser, err := postgres.FindUserByUID(ctx, tx, uid) - if err != nil { - return nil, err + userData := &user.UserWithProfileImage{ + ID: int(row.ID), + Email: row.Email, + Nickname: row.Nickname, + Fullname: row.Fullname, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), + FirebaseUID: row.FbUid.String, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, } - if err := tx.Commit(); err != nil { - return nil, err - } - - return foundUser.ToFindUserView(), nil + return userData.ToFindUserView(), nil } func (service *UserService) ExistsByNickname(ctx context.Context, nickname string) (bool, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return false, err - } - - existsByNickname, err := postgres.ExistsUserByNickname(ctx, tx, nickname) + existsByNickname, err := databasegen.New(service.conn).ExistsUserByNickname(ctx, nickname) if err != nil { - return existsByNickname, err - } - - if err := tx.Commit(); err != nil { - return existsByNickname, err + return existsByNickname, pnd.FromPostgresError(err) } return existsByNickname, nil } func (service *UserService) FindUserStatusByEmail(ctx context.Context, email string) (*user.UserStatus, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() + userData, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + Email: utils.StrToNullStr(email), + IncludeDeleted: false, + }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - userStatus, err := postgres.FindUserStatusByEmail(ctx, tx, email) - if err != nil { - return userStatus, err + userStatus := &user.UserStatus{ + FirebaseProviderType: user.FirebaseProviderType(userData.FbProviderType.String), } - - if err := tx.Commit(); err != nil { - return userStatus, err - } - return userStatus, nil } @@ -207,9 +260,13 @@ func (service *UserService) UpdateUserByUID( return nil, err } - updatedUser, err := postgres.UpdateUserByUID(ctx, tx, uid, nickname, profileImageID) - if err != nil { - return nil, err + row, err2 := databasegen.New(service.conn).WithTx(tx.Tx).UpdateUserByFbUID(ctx, databasegen.UpdateUserByFbUIDParams{ + Nickname: nickname, + ProfileImageID: utils.IntPtrToNullInt64(profileImageID), + FbUid: utils.StrToNullStr(uid), + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } if err := tx.Commit(); err != nil { @@ -217,8 +274,8 @@ func (service *UserService) UpdateUserByUID( } var profileImageURL *string - if updatedUser.ProfileImageID != nil { - profileImage, err := service.mediaService.FindMediaByID(ctx, *updatedUser.ProfileImageID) + if !row.ProfileImageID.Valid { + profileImage, err := service.mediaService.FindMediaByID(ctx, int(row.ProfileImageID.Int64)) if err != nil { return nil, err } @@ -228,7 +285,19 @@ func (service *UserService) UpdateUserByUID( } } - return updatedUser.ToUserWithProfileImage(profileImageURL), nil + updatedUser := user.UserWithProfileImage{ + ID: int(row.ID), + Email: row.Email, + Nickname: row.Nickname, + Fullname: row.Fullname, + ProfileImageURL: profileImageURL, + FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), + FirebaseUID: row.FbUid.String, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + } + + return &updatedUser, nil } func (service *UserService) DeleteUserByUID(ctx context.Context, uid string) *pnd.AppError { @@ -238,8 +307,8 @@ func (service *UserService) DeleteUserByUID(ctx context.Context, uid string) *pn return err } - if err := postgres.DeleteUserByUID(ctx, tx, uid); err != nil { - return err + if err := databasegen.New(service.conn).WithTx(tx.Tx).DeleteUserByFbUID(ctx, utils.StrToNullStr(uid)); err != nil { + return pnd.FromPostgresError(err) } return tx.Commit() @@ -254,9 +323,11 @@ func (service *UserService) AddPetsToOwner( return nil, err } - userData, err := postgres.FindUserByUID(ctx, tx, uid) - if err != nil { - return nil, err + userData, err2 := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + FbUid: utils.StrToNullStr(uid), + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } pets := make(pet.PetWithProfileList, len(addPetsRequest.Pets)) @@ -267,7 +338,7 @@ func (service *UserService) AddPetsToOwner( } } - petToCreate := item.ToPet(userData.ID) + petToCreate := item.ToPet(int(userData.ID)) createdPet, err := postgres.CreatePet(ctx, tx, petToCreate) if err != nil { return nil, err @@ -355,18 +426,21 @@ func (service *UserService) DeletePet(ctx context.Context, uid string, petID int } func (service *UserService) FindPetsByOwnerUID(ctx context.Context, uid string) (*pet.FindMyPetsView, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return nil, err + userData, err2 := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + FbUid: utils.StrToNullStr(uid), + IncludeDeleted: false, + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } - userData, err := postgres.FindUserByUID(ctx, tx, uid) + tx, err := service.conn.BeginTx(ctx) + defer tx.Rollback() if err != nil { return nil, err } - pets, err := postgres.FindPetsByOwnerID(ctx, tx, userData.ID) + pets, err := postgres.FindPetsByOwnerID(ctx, tx, int(userData.ID)) if err != nil { return nil, err } diff --git a/queries/users.sql b/queries/users.sql index 86bc85cc..19fef19a 100644 --- a/queries/users.sql +++ b/queries/users.sql @@ -21,12 +21,15 @@ FROM users media ON users.profile_image_id = media.id -WHERE (users.nickname = $1 OR $1 IS NULL) - AND users.deleted_at IS NULL +WHERE (users.id = sqlc.narg('id') OR sqlc.narg('id') IS NULL) + AND (users.nickname = sqlc.narg('nickname') OR sqlc.narg('nickname') IS NULL) + AND (users.email = sqlc.narg('email') OR sqlc.narg('email') IS NULL) + AND (users.fb_uid = sqlc.narg('fb_uid') OR sqlc.narg('fb_uid') IS NULL) + AND (users.deleted_at IS NULL OR sqlc.arg('include_deleted')::boolean = TRUE) ORDER BY users.created_at DESC -LIMIT $2 OFFSET $3; +LIMIT $1 OFFSET $2; --- name: FindUsersByID :one +-- name: FindUser :one SELECT users.id, users.email, users.nickname, @@ -42,50 +45,11 @@ FROM users media ON users.profile_image_id = media.id -WHERE users.id = $1 - AND (users.deleted_at IS NULL OR $2); - --- name: FindUserByEmail :one -SELECT users.id, - users.email, - users.nickname, - users.fullname, - media.url AS profile_image_url, - users.fb_provider_type, - users.fb_uid, - users.created_at, - users.updated_at -FROM users - LEFT OUTER JOIN - media - ON - users.profile_image_id = media.id -WHERE users.email = $1 - AND users.deleted_at IS NULL; - --- name: FindUserByUID :one -SELECT users.id, - users.email, - users.nickname, - users.fullname, - media.url AS profile_image_url, - users.fb_provider_type, - users.fb_uid, - users.created_at, - users.updated_at -FROM users - LEFT JOIN - media - ON - users.profile_image_id = media.id -WHERE users.fb_uid = $1 - AND users.deleted_at IS NULL; - --- name: FindUserIDByFbUID :one -SELECT id -FROM users -WHERE fb_uid = $1 - AND deleted_at IS NULL; +WHERE (users.id = sqlc.narg('id') OR sqlc.narg('id') IS NULL) + AND (users.nickname = sqlc.narg('nickname') OR sqlc.narg('nickname') IS NULL) + AND (users.email = sqlc.narg('email') OR sqlc.narg('email') IS NULL) + AND (users.fb_uid = sqlc.narg('fb_uid') OR sqlc.narg('fb_uid') IS NULL) + AND (users.deleted_at IS NULL OR sqlc.arg('include_deleted')::boolean = TRUE); -- name: ExistsUserByNickname :one SELECT CASE @@ -98,13 +62,7 @@ SELECT CASE ELSE FALSE END; --- name: FindUserStatusByEmail :one -SELECT fb_provider_type -FROM users -WHERE email = $1 - AND deleted_at IS NULL; - --- name: UpdateUserByUID :one +-- name: UpdateUserByFbUID :one UPDATE users SET nickname = $1, @@ -123,7 +81,7 @@ RETURNING created_at, updated_at; --- name: DeleteUserByUID :exec +-- name: DeleteUserByFbUID :exec UPDATE users SET deleted_at = NOW() From ae353686d2e1bf2e779d29c774fc6aa4b4a0b079 Mon Sep 17 00:00:00 2001 From: litsynp Date: Wed, 24 Apr 2024 02:39:07 +0900 Subject: [PATCH 04/12] feat: streamline user service by reducing number of functions --- cmd/server/handler/user_handler.go | 27 +- internal/domain/sospost/view.go | 34 +-- internal/domain/user/model.go | 77 ++++++ internal/domain/user/params.go | 39 +++ internal/domain/user/request.go | 40 +++ internal/domain/user/user.go | 59 ----- internal/domain/user/view.go | 127 ++++------ internal/service/auth_service.go | 8 +- internal/service/sos_post_service.go | 6 +- .../service/tests/sos_post_service_test.go | 12 +- internal/service/tests/user_service_test.go | 42 +--- internal/service/user_service.go | 232 +++--------------- internal/tests/service.go | 2 +- 13 files changed, 283 insertions(+), 422 deletions(-) create mode 100644 internal/domain/user/model.go create mode 100644 internal/domain/user/params.go create mode 100644 internal/domain/user/request.go delete mode 100644 internal/domain/user/user.go diff --git a/cmd/server/handler/user_handler.go b/cmd/server/handler/user_handler.go index 00ccb22e..f9381aed 100644 --- a/cmd/server/handler/user_handler.go +++ b/cmd/server/handler/user_handler.go @@ -30,7 +30,7 @@ func NewUserHandler(userService service.UserService, authService service.AuthSer // @Accept json // @Produce json // @Param request body user.RegisterUserRequest true "사용자 회원가입 요청" -// @Success 201 {object} user.RegisterUserView +// @Success 201 {object} user.InternalView // @Router /users [post] func (h *UserHandler) RegisterUser(c echo.Context) error { var registerUserRequest user.RegisterUserRequest @@ -76,7 +76,7 @@ func (h *UserHandler) CheckUserNickname(c echo.Context) error { // @Accept json // @Produce json // @Param request body user.UserStatusRequest true "사용자 가입 상태 조회 요청" -// @Success 200 {object} user.UserStatusView +// @Success 200 {object} user.StatusView // @Router /users/status [post] func (h *UserHandler) FindUserStatusByEmail(c echo.Context) error { var providerRequest user.UserStatusRequest @@ -84,14 +84,14 @@ func (h *UserHandler) FindUserStatusByEmail(c echo.Context) error { return c.JSON(err.StatusCode, err) } - userStatus, err := h.userService.FindUserStatusByEmail(c.Request().Context(), providerRequest.Email) - if err != nil || userStatus == nil { - return c.JSON(http.StatusOK, user.UserStatusView{ - Status: user.UserStatusNotRegistered, + userData, err := h.userService.FindUser(c.Request().Context(), user.FindUserParams{Email: &providerRequest.Email}) + if err != nil { + return c.JSON(http.StatusOK, user.StatusView{ + Status: user.StatusNotRegistered, }) } - return c.JSON(http.StatusOK, userStatus.ToUserStatusView()) + return c.JSON(http.StatusOK, user.NewStatusView(userData.FirebaseProviderType)) } // FindUsers godoc @@ -103,7 +103,7 @@ func (h *UserHandler) FindUserStatusByEmail(c echo.Context) error { // @Param page query int false "페이지 번호" default(1) // @Param size query int false "페이지 사이즈" default(10) // @Param nickname query string false "닉네임 (완전 일치)" -// @Success 200 {object} user.UserWithoutPrivateInfoList +// @Success 200 {object} user.ListWithoutPrivateInfo // @Router /users [get] func (h *UserHandler) FindUsers(c echo.Context) error { _, err := h.authService.VerifyAuthAndGetUser(c.Request().Context(), c.Request().Header.Get("Authorization")) @@ -117,9 +117,10 @@ func (h *UserHandler) FindUsers(c echo.Context) error { return c.JSON(err.StatusCode, err) } - var res *user.UserWithoutPrivateInfoList + var res *user.ListWithoutPrivateInfo - res, err = h.userService.FindUsers(c.Request().Context(), page, size, nickname) + res, err = h.userService.FindUsers(c.Request().Context(), + user.FindUsersParams{Page: page, Size: size, Nickname: nickname}) if err != nil { return c.JSON(err.StatusCode, err) } @@ -152,7 +153,7 @@ func (h *UserHandler) FindMyProfile(c echo.Context) error { // @Produce json // @Security FirebaseAuth // @Param request body user.UpdateUserRequest true "사용자 프로필 수정 요청" -// @Success 200 {object} user.UpdateUserView +// @Success 200 {object} user.MyProfileView // @Router /users/me [put] func (h *UserHandler) UpdateMyProfile(c echo.Context) error { foundUser, err := h.authService.VerifyAuthAndGetUser(c.Request().Context(), c.Request().Header.Get("Authorization")) @@ -167,7 +168,7 @@ func (h *UserHandler) UpdateMyProfile(c echo.Context) error { return c.JSON(err.StatusCode, err) } - userModel, err := h.userService.UpdateUserByUID( + view, err := h.userService.UpdateUserByUID( c.Request().Context(), uid, updateUserRequest.Nickname, @@ -177,7 +178,7 @@ func (h *UserHandler) UpdateMyProfile(c echo.Context) error { return c.JSON(err.StatusCode, err) } - return c.JSON(http.StatusOK, userModel.ToUpdateUserView()) + return c.JSON(http.StatusOK, view) } // DeleteMyAccount godoc diff --git a/internal/domain/sospost/view.go b/internal/domain/sospost/view.go index 5f6ee739..4cfacced 100644 --- a/internal/domain/sospost/view.go +++ b/internal/domain/sospost/view.go @@ -87,25 +87,25 @@ func (p *SOSPost) ToWriteSOSPostView( } type FindSOSPostView struct { - ID int `json:"id"` - Author *user.UserWithoutPrivateInfo `json:"author"` - Title string `json:"title"` - Content string `json:"content"` - Media media.MediaViewList `json:"media"` - Conditions []ConditionView `json:"conditions"` - Pets []pet.PetView `json:"pets"` - Reward string `json:"reward"` - Dates []SOSDateView `json:"dates"` - CareType CareType `json:"careType"` - CarerGender CarerGender `json:"carerGender"` - RewardType RewardType `json:"rewardType"` - ThumbnailID int `json:"thumbnailId"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + ID int `json:"id"` + Author *user.WithoutPrivateInfo `json:"author"` + Title string `json:"title"` + Content string `json:"content"` + Media media.MediaViewList `json:"media"` + Conditions []ConditionView `json:"conditions"` + Pets []pet.PetView `json:"pets"` + Reward string `json:"reward"` + Dates []SOSDateView `json:"dates"` + CareType CareType `json:"careType"` + CarerGender CarerGender `json:"carerGender"` + RewardType RewardType `json:"rewardType"` + ThumbnailID int `json:"thumbnailId"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } func (p *SOSPost) ToFindSOSPostView( - author *user.UserWithoutPrivateInfo, + author *user.WithoutPrivateInfo, mediaList media.MediaViewList, conditions []ConditionView, pets []pet.PetView, @@ -151,7 +151,7 @@ func FromEmptySOSPostInfoList(sosPosts *SOSPostInfoList) *FindSOSPostListView { } func (p *SOSPostInfo) ToFindSOSPostInfoView( - author *user.UserWithoutPrivateInfo, + author *user.WithoutPrivateInfo, mediaList media.MediaViewList, conditions []ConditionView, pets []pet.PetView, diff --git a/internal/domain/user/model.go b/internal/domain/user/model.go new file mode 100644 index 00000000..76c301d5 --- /dev/null +++ b/internal/domain/user/model.go @@ -0,0 +1,77 @@ +package user + +import ( + "database/sql" + "time" + + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" +) + +type FirebaseProviderType string + +const ( + FirebaseProviderTypeEmail FirebaseProviderType = "email" + FirebaseProviderTypeGoogle FirebaseProviderType = "google" + FirebaseProviderTypeApple FirebaseProviderType = "apple" + FirebaseProviderTypeKakao FirebaseProviderType = "kakao" +) + +func (f FirebaseProviderType) String() string { + return string(f) +} + +func (f FirebaseProviderType) NullString() sql.NullString { + return sql.NullString{String: string(f), Valid: true} +} + +type WithProfileImage struct { + ID int + Email string + Password string + Nickname string + Fullname string + ProfileImageURL *string + FirebaseProviderType FirebaseProviderType + FirebaseUID string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +func ToWithProfileImage(row databasegen.FindUserRow) *WithProfileImage { + return &WithProfileImage{ + ID: int(row.ID), + Email: row.Email, + Nickname: row.Nickname, + Fullname: row.Fullname, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + FirebaseProviderType: FirebaseProviderType(row.FbProviderType.String), + FirebaseUID: row.FbUid.String, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + } +} + +func (u *WithProfileImage) ToInternalView() *InternalView { + return &InternalView{ + ID: u.ID, + Email: u.Email, + Nickname: u.Nickname, + Fullname: u.Fullname, + ProfileImageURL: u.ProfileImageURL, + FirebaseProviderType: u.FirebaseProviderType, + FirebaseUID: u.FirebaseUID, + } +} + +func (u *WithProfileImage) ToMyProfileView() *MyProfileView { + return &MyProfileView{ + ID: u.ID, + Email: u.Email, + Nickname: u.Nickname, + Fullname: u.Fullname, + ProfileImageURL: u.ProfileImageURL, + FirebaseProviderType: u.FirebaseProviderType, + } +} diff --git a/internal/domain/user/params.go b/internal/domain/user/params.go new file mode 100644 index 00000000..3d65da73 --- /dev/null +++ b/internal/domain/user/params.go @@ -0,0 +1,39 @@ +package user + +import ( + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" +) + +type FindUserParams struct { + ID *int + Email *string + FbUID *string + IncludeDeleted bool +} + +func (p *FindUserParams) ToDBParams() databasegen.FindUserParams { + return databasegen.FindUserParams{ + ID: utils.IntPtrToNullInt32(p.ID), + Email: utils.StrPtrToNullStr(p.Email), + FbUid: utils.StrPtrToNullStr(p.FbUID), + IncludeDeleted: p.IncludeDeleted, + } +} + +type FindUsersParams struct { + Page int + Size int + Nickname *string + IncludeDeleted bool +} + +func (p *FindUsersParams) ToDBParams() databasegen.FindUsersParams { + pagination := utils.OffsetAndLimit(p.Page, p.Size) + return databasegen.FindUsersParams{ + Limit: int32(pagination.Limit), + Offset: int32(pagination.Offset), + Nickname: utils.StrPtrToNullStr(p.Nickname), + IncludeDeleted: p.IncludeDeleted, + } +} diff --git a/internal/domain/user/request.go b/internal/domain/user/request.go new file mode 100644 index 00000000..f35e56b0 --- /dev/null +++ b/internal/domain/user/request.go @@ -0,0 +1,40 @@ +package user + +import ( + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" +) + +type RegisterUserRequest struct { + Email string `json:"email" validate:"required,email"` + Nickname string `json:"nickname" validate:"required"` + Fullname string `json:"fullname" validate:"required"` + ProfileImageID *int `json:"profileImageId"` + FirebaseProviderType FirebaseProviderType `json:"fbProviderType" validate:"required"` + FirebaseUID string `json:"fbUid" validate:"required"` +} + +func (r *RegisterUserRequest) ToDBParams() databasegen.CreateUserParams { + return databasegen.CreateUserParams{ + Email: r.Email, + Nickname: r.Nickname, + Fullname: r.Fullname, + Password: "", + ProfileImageID: utils.IntPtrToNullInt64(r.ProfileImageID), + FbProviderType: r.FirebaseProviderType.NullString(), + FbUid: utils.StrToNullStr(r.FirebaseUID), + } +} + +type CheckNicknameRequest struct { + Nickname string `json:"nickname" validate:"required"` +} + +type UserStatusRequest struct { + Email string `json:"email" validate:"required,email"` +} + +type UpdateUserRequest struct { + Nickname string `json:"nickname" validate:"required"` + ProfileImageID *int `json:"profileImageId" validate:"omitempty"` +} diff --git a/internal/domain/user/user.go b/internal/domain/user/user.go deleted file mode 100644 index 29de8128..00000000 --- a/internal/domain/user/user.go +++ /dev/null @@ -1,59 +0,0 @@ -package user - -import ( - "database/sql" - "time" - - pnd "github.com/pet-sitter/pets-next-door-api/api" -) - -type FirebaseProviderType string - -const ( - FirebaseProviderTypeEmail FirebaseProviderType = "email" - FirebaseProviderTypeGoogle FirebaseProviderType = "google" - FirebaseProviderTypeApple FirebaseProviderType = "apple" - FirebaseProviderTypeKakao FirebaseProviderType = "kakao" -) - -func (f FirebaseProviderType) String() string { - return string(f) -} - -func (f FirebaseProviderType) NullString() sql.NullString { - return sql.NullString{String: string(f), Valid: true} -} - -type UserWithProfileImage struct { - ID int - Email string - Password string - Nickname string - Fullname string - ProfileImageURL *string - FirebaseProviderType FirebaseProviderType - FirebaseUID string - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt sql.NullTime -} - -type UserWithoutPrivateInfo struct { - ID int `field:"id" json:"id"` - Nickname string `field:"nickname" json:"nickname"` - ProfileImageURL *string `field:"profile_image_url" json:"profileImageUrl"` -} - -type UserWithoutPrivateInfoList struct { - *pnd.PaginatedView[UserWithoutPrivateInfo] -} - -func NewUserWithoutPrivateInfoList(page, size int) *UserWithoutPrivateInfoList { - return &UserWithoutPrivateInfoList{PaginatedView: pnd.NewPaginatedView( - page, size, false, make([]UserWithoutPrivateInfo, 0), - )} -} - -type UserStatus struct { - FirebaseProviderType FirebaseProviderType `field:"fb_provider_type"` -} diff --git a/internal/domain/user/view.go b/internal/domain/user/view.go index 5f6c2d45..6761f6cc 100644 --- a/internal/domain/user/view.go +++ b/internal/domain/user/view.go @@ -1,39 +1,12 @@ package user -import databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" - -type RegisterUserRequest struct { - Email string `json:"email" validate:"required,email"` - Nickname string `json:"nickname" validate:"required"` - Fullname string `json:"fullname" validate:"required"` - ProfileImageID *int `json:"profileImageId"` - FirebaseProviderType FirebaseProviderType `json:"fbProviderType" validate:"required"` - FirebaseUID string `json:"fbUid" validate:"required"` -} - -type RegisterUserView struct { - ID int `json:"id"` - Email string `json:"email"` - Nickname string `json:"nickname"` - Fullname string `json:"fullname"` - ProfileImageURL *string `json:"profileImageUrl"` - FirebaseProviderType FirebaseProviderType `json:"fbProviderType"` - FirebaseUID string `json:"fbUid"` -} - -func ToRegisterUserView(u *databasegen.CreateUserRow, profileImageURL *string) *RegisterUserView { - return &RegisterUserView{ - ID: int(u.ID), - Email: u.Email, - Nickname: u.Nickname, - Fullname: u.Fullname, - ProfileImageURL: profileImageURL, - FirebaseProviderType: FirebaseProviderType(u.FbProviderType.String), - FirebaseUID: u.FbUid.String, - } -} +import ( + pnd "github.com/pet-sitter/pets-next-door-api/api" + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" +) -type FindUserView struct { +type InternalView struct { ID int `json:"id"` Email string `json:"email"` Nickname string `json:"nickname"` @@ -43,19 +16,7 @@ type FindUserView struct { FirebaseUID string `json:"fbUid"` } -func (u *UserWithProfileImage) ToFindUserView() *FindUserView { - return &FindUserView{ - ID: u.ID, - Email: u.Email, - Nickname: u.Nickname, - Fullname: u.Fullname, - ProfileImageURL: u.ProfileImageURL, - FirebaseProviderType: u.FirebaseProviderType, - FirebaseUID: u.FirebaseUID, - } -} - -func (r *FindUserView) ToMyProfileView() *MyProfileView { +func (r *InternalView) ToMyProfileView() *MyProfileView { return &MyProfileView{ ID: r.ID, Email: r.Email, @@ -75,58 +36,60 @@ type MyProfileView struct { FirebaseProviderType FirebaseProviderType `json:"fbProviderType"` } -type CheckNicknameRequest struct { - Nickname string `json:"nickname" validate:"required"` -} - type CheckNicknameView struct { IsAvailable bool `json:"isAvailable"` } -type UserStatusRequest struct { - Email string `json:"email" validate:"required,email"` -} - -type UserRegistrationStatus string +type RegistrationStatus string const ( - UserStatusNotRegistered UserRegistrationStatus = "NOT_REGISTERED" - UserStatusRegistered UserRegistrationStatus = "REGISTERED" + StatusNotRegistered RegistrationStatus = "NOT_REGISTERED" + StatusRegistered RegistrationStatus = "REGISTERED" ) -type UserStatusView struct { - Status UserRegistrationStatus `json:"status"` - FirebaseProviderType FirebaseProviderType `json:"fbProviderType,omitempty"` +type StatusView struct { + Status RegistrationStatus `json:"status"` + FirebaseProviderType FirebaseProviderType `json:"fbProviderType,omitempty"` } -func (s *UserStatus) ToUserStatusView() *UserStatusView { - return &UserStatusView{ - Status: UserStatusRegistered, - FirebaseProviderType: s.FirebaseProviderType, +func NewStatusView(providerType FirebaseProviderType) *StatusView { + return &StatusView{ + Status: StatusRegistered, + FirebaseProviderType: providerType, } } -type UpdateUserRequest struct { - Nickname string `json:"nickname" validate:"required"` - ProfileImageID *int `json:"profileImageId" validate:"omitempty"` +type WithoutPrivateInfo struct { + ID int `json:"id"` + Nickname string `json:"nickname"` + ProfileImageURL *string `json:"profileImageUrl"` } -type UpdateUserView struct { - ID int `json:"id"` - Email string `json:"email"` - Nickname string `json:"nickname"` - Fullname string `json:"fullname"` - ProfileImageURL *string `json:"profileImageUrl"` - FirebaseProviderType FirebaseProviderType `json:"fbProviderType"` +func ToWithoutPrivateInfo(row databasegen.FindUserRow) *WithoutPrivateInfo { + return &WithoutPrivateInfo{ + ID: int(row.ID), + Nickname: row.Nickname, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + } +} + +type ListWithoutPrivateInfo struct { + *pnd.PaginatedView[WithoutPrivateInfo] } -func (u *UserWithProfileImage) ToUpdateUserView() *UpdateUserView { - return &UpdateUserView{ - ID: u.ID, - Email: u.Email, - Nickname: u.Nickname, - Fullname: u.Fullname, - ProfileImageURL: u.ProfileImageURL, - FirebaseProviderType: u.FirebaseProviderType, +func ToListWithoutPrivateInfo(page, size int, rows []databasegen.FindUsersRow) *ListWithoutPrivateInfo { + ul := &ListWithoutPrivateInfo{PaginatedView: pnd.NewPaginatedView( + page, size, false, make([]WithoutPrivateInfo, 0), + )} + + for _, row := range rows { + ul.Items = append(ul.Items, WithoutPrivateInfo{ + ID: int(row.ID), + Nickname: row.Nickname, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + }) } + + ul.CalcLastPage() + return ul } diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 4ecca7c7..0a4b9a85 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -11,7 +11,7 @@ import ( ) type AuthService interface { - VerifyAuthAndGetUser(ctx context.Context, authHeader string) (*user.FindUserView, *pnd.AppError) + VerifyAuthAndGetUser(ctx context.Context, authHeader string) (*user.InternalView, *pnd.AppError) CustomToken(ctx context.Context, uid string) (*string, *pnd.AppError) } @@ -45,18 +45,18 @@ func (service *FirebaseBearerAuthService) verifyAuth( func (service *FirebaseBearerAuthService) VerifyAuthAndGetUser( ctx context.Context, authHeader string, -) (*user.FindUserView, *pnd.AppError) { +) (*user.InternalView, *pnd.AppError) { authToken, err := service.verifyAuth(ctx, authHeader) if err != nil { return nil, err } - foundUser, err := service.userService.FindUserByUID(ctx, authToken.UID) + foundUser, err := service.userService.FindUser(ctx, user.FindUserParams{FbUID: &authToken.UID}) if err != nil { return nil, pnd.ErrUserNotRegistered(errors.New("가입되지 않은 사용자입니다")) } - return foundUser, nil + return foundUser.ToInternalView(), nil } func (service *FirebaseBearerAuthService) CustomToken(ctx context.Context, uid string) (*string, *pnd.AppError) { diff --git a/internal/service/sos_post_service.go b/internal/service/sos_post_service.go index a2bad61a..7c4bb451 100644 --- a/internal/service/sos_post_service.go +++ b/internal/service/sos_post_service.go @@ -104,7 +104,7 @@ func (service *SOSPostService) FindSOSPosts( } sosPostView := sosPost.ToFindSOSPostInfoView( - &user.UserWithoutPrivateInfo{ + &user.WithoutPrivateInfo{ ID: int(author.ID), Nickname: author.Nickname, ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), @@ -146,7 +146,7 @@ func (service *SOSPostService) FindSOSPostsByAuthorID( } sosPostView := sosPost.ToFindSOSPostInfoView( - &user.UserWithoutPrivateInfo{ + &user.WithoutPrivateInfo{ ID: int(author.ID), Nickname: author.Nickname, ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), @@ -187,7 +187,7 @@ func (service *SOSPostService) FindSOSPostByID(ctx context.Context, id int) (*so } return sosPost.ToFindSOSPostInfoView( - &user.UserWithoutPrivateInfo{ + &user.WithoutPrivateInfo{ ID: int(author.ID), Nickname: author.Nickname, ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), diff --git a/internal/service/tests/sos_post_service_test.go b/internal/service/tests/sos_post_service_test.go index fc70d190..110212c7 100644 --- a/internal/service/tests/sos_post_service_test.go +++ b/internal/service/tests/sos_post_service_test.go @@ -111,7 +111,7 @@ func TestSOSPostService(t *testing.T) { userService := service.NewUserService(db, mediaService) userRequest := tests.GenerateDummyRegisterUserRequest(&profileImage.ID) owner, _ := userService.RegisterUser(ctx, userRequest) - author := &user.UserWithoutPrivateInfo{ + author := &user.WithoutPrivateInfo{ ID: owner.ID, ProfileImageURL: owner.ProfileImageURL, Nickname: owner.Nickname, @@ -161,7 +161,7 @@ func TestSOSPostService(t *testing.T) { userService := service.NewUserService(db, mediaService) userRequest := tests.GenerateDummyRegisterUserRequest(&profileImage.ID) owner, _ := userService.RegisterUser(ctx, userRequest) - author := &user.UserWithoutPrivateInfo{ + author := &user.WithoutPrivateInfo{ ID: owner.ID, ProfileImageURL: owner.ProfileImageURL, Nickname: owner.Nickname, @@ -210,7 +210,7 @@ func TestSOSPostService(t *testing.T) { userService := service.NewUserService(db, mediaService) userRequest := tests.GenerateDummyRegisterUserRequest(&profileImage.ID) owner, _ := userService.RegisterUser(ctx, userRequest) - author := &user.UserWithoutPrivateInfo{ + author := &user.WithoutPrivateInfo{ ID: owner.ID, ProfileImageURL: owner.ProfileImageURL, Nickname: owner.Nickname, @@ -267,7 +267,7 @@ func TestSOSPostService(t *testing.T) { userService := service.NewUserService(db, mediaService) userRequest := tests.GenerateDummyRegisterUserRequest(&profileImage.ID) owner, _ := userService.RegisterUser(ctx, userRequest) - author := &user.UserWithoutPrivateInfo{ + author := &user.WithoutPrivateInfo{ ID: owner.ID, ProfileImageURL: owner.ProfileImageURL, Nickname: owner.Nickname, @@ -317,7 +317,7 @@ func TestSOSPostService(t *testing.T) { userService := service.NewUserService(db, mediaService) userRequest := tests.GenerateDummyRegisterUserRequest(&profileImage.ID) owner, _ := userService.RegisterUser(ctx, userRequest) - author := &user.UserWithoutPrivateInfo{ + author := &user.WithoutPrivateInfo{ ID: owner.ID, ProfileImageURL: owner.ProfileImageURL, Nickname: owner.Nickname, @@ -488,7 +488,7 @@ func assertMediaEquals(t *testing.T, got, want media.MediaViewList) { } } -func assertAuthorEquals(t *testing.T, got, want *user.UserWithoutPrivateInfo) { +func assertAuthorEquals(t *testing.T, got, want *user.WithoutPrivateInfo) { t.Helper() if !reflect.DeepEqual(got, want) { diff --git a/internal/service/tests/user_service_test.go b/internal/service/tests/user_service_test.go index c56ac33a..9a41052b 100644 --- a/internal/service/tests/user_service_test.go +++ b/internal/service/tests/user_service_test.go @@ -5,7 +5,6 @@ import ( "fmt" "testing" - "github.com/pet-sitter/pets-next-door-api/internal/domain/media" "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/domain/user" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" @@ -130,7 +129,7 @@ func TestUserService(t *testing.T) { } // When - found, _ := userService.FindUsers(ctx, 1, 20, &targetNickname) + found, _ := userService.FindUsers(ctx, user.FindUsersParams{Page: 1, Size: 20, Nickname: &targetNickname}) // Then if len(found.Items) != 1 { @@ -154,7 +153,7 @@ func TestUserService(t *testing.T) { created, _ := userService.RegisterUser(ctx, userRequest) // When - found, _ := userService.FindUserByEmail(ctx, created.Email) + found, _ := userService.FindUser(ctx, user.FindUserParams{Email: &created.Email}) // Then if found.Email != userRequest.Email { @@ -171,7 +170,8 @@ func TestUserService(t *testing.T) { userService := service.NewUserService(db, nil) // When - _, err := userService.FindUserByEmail(ctx, "non-existent@example.com") + email := "non-existent@example.com" + _, err := userService.FindUser(ctx, user.FindUserParams{Email: &email}) // Then if err == nil { @@ -195,7 +195,7 @@ func TestUserService(t *testing.T) { created, _ := userService.RegisterUser(ctx, userRequest) // When - found, _ := userService.FindUserByUID(ctx, created.FirebaseUID) + found, _ := userService.FindUser(ctx, user.FindUserParams{FbUID: &created.FirebaseUID}) // Then if found.FirebaseUID != userRequest.FirebaseUID { @@ -212,7 +212,8 @@ func TestUserService(t *testing.T) { userService := service.NewUserService(db, nil) // When - _, err := userService.FindUserByUID(ctx, "non-existent") + fbUID := "non-existent" + _, err := userService.FindUser(ctx, user.FindUserParams{FbUID: &fbUID}) // Then if err == nil { @@ -262,33 +263,6 @@ func TestUserService(t *testing.T) { }) }) - t.Run("FindUserStatusByEmail", func(t *testing.T) { - t.Run("사용자의 상태를 반환한다", func(t *testing.T) { - db, tearDown := setUp(t) - defer tearDown(t) - ctx := context.Background() - - // Given - mediaService := service.NewMediaService(db, nil) - profileImage, _ := mediaService.CreateMedia(ctx, &media.Media{ - MediaType: media.MediaTypeImage, - URL: "http://example.com", - }) - - userService := service.NewUserService(db, mediaService) - userRequest := tests.GenerateDummyRegisterUserRequest(&profileImage.ID) - created, _ := userService.RegisterUser(ctx, userRequest) - - // When - status, _ := userService.FindUserStatusByEmail(ctx, created.Email) - - // Then - if status.FirebaseProviderType != userRequest.FirebaseProviderType { - t.Errorf("got %v want %v", status.FirebaseProviderType, userRequest.FirebaseProviderType) - } - }) - }) - t.Run("UpdateUserByUID", func(t *testing.T) { t.Run("사용자를 업데이트한다", func(t *testing.T) { db, tearDown := setUp(t) @@ -307,7 +281,7 @@ func TestUserService(t *testing.T) { userService.UpdateUserByUID(ctx, userRequest.FirebaseUID, updatedNickname, &updatedProfileImage.ID) // Then - found, _ := userService.FindUserByUID(ctx, userRequest.FirebaseUID) + found, _ := userService.FindUser(ctx, user.FindUserParams{FbUID: &userRequest.FirebaseUID}) if found.Nickname != updatedNickname { t.Errorf("got %v want %v", found.Nickname, updatedNickname) } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index baa460a2..9424c9e8 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -29,17 +29,12 @@ func NewUserService(conn *database.DB, mediaService *MediaService) *UserService func (service *UserService) RegisterUser( ctx context.Context, registerUserRequest *user.RegisterUserRequest, -) (*user.RegisterUserView, *pnd.AppError) { - var profileImageURL *string +) (*user.InternalView, *pnd.AppError) { if registerUserRequest.ProfileImageID != nil { - mediaData, err := service.mediaService.FindMediaByID(ctx, *registerUserRequest.ProfileImageID) + _, err := service.mediaService.FindMediaByID(ctx, *registerUserRequest.ProfileImageID) if err != nil { return nil, err } - - if mediaData != nil { - profileImageURL = &mediaData.URL - } } tx, err := service.conn.BeginTx(ctx) @@ -48,15 +43,7 @@ func (service *UserService) RegisterUser( return nil, err } - created, err2 := databasegen.New(service.conn).WithTx(tx.Tx).CreateUser(ctx, databasegen.CreateUserParams{ - Email: registerUserRequest.Email, - Nickname: registerUserRequest.Nickname, - Fullname: registerUserRequest.Fullname, - Password: "", - ProfileImageID: utils.IntPtrToNullInt64(registerUserRequest.ProfileImageID), - FbProviderType: registerUserRequest.FirebaseProviderType.NullString(), - FbUid: utils.StrToNullStr(registerUserRequest.FirebaseUID), - }) + _, err2 := databasegen.New(service.conn).WithTx(tx.Tx).CreateUser(ctx, registerUserRequest.ToDBParams()) if err2 != nil { return nil, pnd.FromPostgresError(err2) } @@ -65,166 +52,37 @@ func (service *UserService) RegisterUser( return nil, err } - return user.ToRegisterUserView(&created, profileImageURL), nil -} - -func (service *UserService) FindUsers( - ctx context.Context, page, size int, nickname *string, -) (*user.UserWithoutPrivateInfoList, *pnd.AppError) { - pagination := utils.OffsetAndLimit(page, size) - rows, err := databasegen.New(service.conn).FindUsers(ctx, databasegen.FindUsersParams{ - Limit: int32(pagination.Limit), - Offset: int32(pagination.Offset), - Nickname: utils.StrPtrToNullStr(nickname), - IncludeDeleted: false, - }) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - - userList := user.NewUserWithoutPrivateInfoList(page, size) - for _, row := range rows { - userList.Items = append(userList.Items, user.UserWithoutPrivateInfo{ - ID: int(row.ID), - Nickname: row.Nickname, - ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), - }) - } - userList.CalcLastPage() - - return userList, nil -} - -type FindUserParams struct { - ID *int - Email *string - FbUID *string - IncludeDeleted bool -} - -func (service *UserService) FindUser( - ctx context.Context, - params FindUserParams, -) (*user.UserWithProfileImage, *pnd.AppError) { - row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ - ID: utils.IntPtrToNullInt32(params.ID), - Email: utils.StrPtrToNullStr(params.Email), - FbUid: utils.StrPtrToNullStr(params.FbUID), - IncludeDeleted: params.IncludeDeleted, - }) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - - userData := &user.UserWithProfileImage{ - ID: int(row.ID), - Email: row.Email, - Nickname: row.Nickname, - Fullname: row.Fullname, - ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), - FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), - FirebaseUID: row.FbUid.String, - CreatedAt: row.CreatedAt, - UpdatedAt: row.UpdatedAt, - } - - return userData, nil -} - -func (service *UserService) findUserByUID(ctx context.Context, uid string) (*user.UserWithProfileImage, *pnd.AppError) { - row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ - FbUid: utils.StrToNullStr(uid), - IncludeDeleted: false, - }) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - - userData := &user.UserWithProfileImage{ - ID: int(row.ID), - Email: row.Email, - Nickname: row.Nickname, - Fullname: row.Fullname, - ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), - FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), - FirebaseUID: row.FbUid.String, - CreatedAt: row.CreatedAt, - UpdatedAt: row.UpdatedAt, - } - - return userData, nil -} - -// FindMyProfile은 사용자의 프로필 정보를 조회한다. -// 삭제된 유저의 경우 삭제된 유저 정보를 반환한다. -func (service *UserService) FindPublicUserByID( - ctx context.Context, id int, -) (*user.UserWithoutPrivateInfo, *pnd.AppError) { - row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ - ID: utils.IntToNullInt32(id), - IncludeDeleted: true, + row, err2 := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + FbUid: utils.StrToNullStr(registerUserRequest.FirebaseUID), }) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - - userData := &user.UserWithoutPrivateInfo{ - ID: int(row.ID), - Nickname: row.Nickname, - ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } - return userData, nil + return user.ToWithProfileImage(row).ToInternalView(), nil } -func (service *UserService) FindUserByEmail( - ctx context.Context, email string, -) (*user.UserWithProfileImage, *pnd.AppError) { - row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ - Email: utils.StrToNullStr(email), - IncludeDeleted: false, - }) +func (service *UserService) FindUsers( + ctx context.Context, params user.FindUsersParams, +) (*user.ListWithoutPrivateInfo, *pnd.AppError) { + rows, err := databasegen.New(service.conn).FindUsers(ctx, params.ToDBParams()) if err != nil { return nil, pnd.FromPostgresError(err) } - userData := &user.UserWithProfileImage{ - ID: int(row.ID), - Email: row.Email, - Nickname: row.Nickname, - Fullname: row.Fullname, - ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), - FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), - FirebaseUID: row.FbUid.String, - CreatedAt: row.CreatedAt, - UpdatedAt: row.UpdatedAt, - } - - return userData, nil + return user.ToListWithoutPrivateInfo(params.Page, params.Size, rows), nil } -func (service *UserService) FindUserByUID(ctx context.Context, uid string) (*user.FindUserView, *pnd.AppError) { - row, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ - FbUid: utils.StrToNullStr(uid), - IncludeDeleted: false, - }) +func (service *UserService) FindUser( + ctx context.Context, + params user.FindUserParams, +) (*user.WithProfileImage, *pnd.AppError) { + row, err := databasegen.New(service.conn).FindUser(ctx, params.ToDBParams()) if err != nil { return nil, pnd.FromPostgresError(err) } - userData := &user.UserWithProfileImage{ - ID: int(row.ID), - Email: row.Email, - Nickname: row.Nickname, - Fullname: row.Fullname, - ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), - FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), - FirebaseUID: row.FbUid.String, - CreatedAt: row.CreatedAt, - UpdatedAt: row.UpdatedAt, - } - - return userData.ToFindUserView(), nil + return user.ToWithProfileImage(row), nil } func (service *UserService) ExistsByNickname(ctx context.Context, nickname string) (bool, *pnd.AppError) { @@ -236,31 +94,16 @@ func (service *UserService) ExistsByNickname(ctx context.Context, nickname strin return existsByNickname, nil } -func (service *UserService) FindUserStatusByEmail(ctx context.Context, email string) (*user.UserStatus, *pnd.AppError) { - userData, err := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ - Email: utils.StrToNullStr(email), - IncludeDeleted: false, - }) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - - userStatus := &user.UserStatus{ - FirebaseProviderType: user.FirebaseProviderType(userData.FbProviderType.String), - } - return userStatus, nil -} - func (service *UserService) UpdateUserByUID( ctx context.Context, uid, nickname string, profileImageID *int, -) (*user.UserWithProfileImage, *pnd.AppError) { +) (*user.MyProfileView, *pnd.AppError) { tx, err := service.conn.BeginTx(ctx) defer tx.Rollback() if err != nil { return nil, err } - row, err2 := databasegen.New(service.conn).WithTx(tx.Tx).UpdateUserByFbUID(ctx, databasegen.UpdateUserByFbUIDParams{ + _, err2 := databasegen.New(service.conn).WithTx(tx.Tx).UpdateUserByFbUID(ctx, databasegen.UpdateUserByFbUIDParams{ Nickname: nickname, ProfileImageID: utils.IntPtrToNullInt64(profileImageID), FbUid: utils.StrToNullStr(uid), @@ -273,31 +116,14 @@ func (service *UserService) UpdateUserByUID( return nil, err } - var profileImageURL *string - if !row.ProfileImageID.Valid { - profileImage, err := service.mediaService.FindMediaByID(ctx, int(row.ProfileImageID.Int64)) - if err != nil { - return nil, err - } - - if profileImage != nil { - profileImageURL = &profileImage.URL - } - } - - updatedUser := user.UserWithProfileImage{ - ID: int(row.ID), - Email: row.Email, - Nickname: row.Nickname, - Fullname: row.Fullname, - ProfileImageURL: profileImageURL, - FirebaseProviderType: user.FirebaseProviderType(row.FbProviderType.String), - FirebaseUID: row.FbUid.String, - CreatedAt: row.CreatedAt, - UpdatedAt: row.UpdatedAt, + refreshedUser, err2 := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ + FbUid: utils.StrToNullStr(uid), + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } - return &updatedUser, nil + return user.ToWithProfileImage(refreshedUser).ToMyProfileView(), nil } func (service *UserService) DeleteUserByUID(ctx context.Context, uid string) *pnd.AppError { @@ -356,7 +182,7 @@ func (service *UserService) AddPetsToOwner( func (service *UserService) UpdatePet( ctx context.Context, uid string, petID int, updatePetRequest pet.UpdatePetRequest, ) (*pet.PetView, *pnd.AppError) { - owner, err := service.findUserByUID(ctx, uid) + owner, err := service.FindUser(ctx, user.FindUserParams{FbUID: &uid, IncludeDeleted: false}) if err != nil { return nil, err } @@ -398,7 +224,7 @@ func (service *UserService) UpdatePet( } func (service *UserService) DeletePet(ctx context.Context, uid string, petID int) *pnd.AppError { - owner, err := service.findUserByUID(ctx, uid) + owner, err := service.FindUser(ctx, user.FindUserParams{FbUID: &uid, IncludeDeleted: false}) if err != nil { return err } diff --git a/internal/tests/service.go b/internal/tests/service.go index 2c68df47..7af4b996 100644 --- a/internal/tests/service.go +++ b/internal/tests/service.go @@ -29,7 +29,7 @@ func RegisterDummyUser( ctx context.Context, userService *service.UserService, mediaService *service.MediaService, -) *user.RegisterUserView { +) *user.InternalView { t.Helper() profileImage := AddDummyMedia(t, ctx, mediaService) userRequest := GenerateDummyRegisterUserRequest(&profileImage.ID) From b72b72f65ec645104ce5224cf9ba6d0f5c7eef7f Mon Sep 17 00:00:00 2001 From: litsynp Date: Wed, 24 Apr 2024 23:06:32 +0900 Subject: [PATCH 05/12] fix: limit 1 for find user query --- internal/infra/database/gen/users.sql.go | 1 + queries/users.sql | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/infra/database/gen/users.sql.go b/internal/infra/database/gen/users.sql.go index b8da3b74..1a099e65 100644 --- a/internal/infra/database/gen/users.sql.go +++ b/internal/infra/database/gen/users.sql.go @@ -125,6 +125,7 @@ WHERE (users.id = $1 OR $1 IS NULL) AND (users.email = $3 OR $3 IS NULL) AND (users.fb_uid = $4 OR $4 IS NULL) AND (users.deleted_at IS NULL OR $5::boolean = TRUE) +LIMIT 1 ` type FindUserParams struct { diff --git a/queries/users.sql b/queries/users.sql index 19fef19a..eebdc301 100644 --- a/queries/users.sql +++ b/queries/users.sql @@ -49,7 +49,8 @@ WHERE (users.id = sqlc.narg('id') OR sqlc.narg('id') IS NULL) AND (users.nickname = sqlc.narg('nickname') OR sqlc.narg('nickname') IS NULL) AND (users.email = sqlc.narg('email') OR sqlc.narg('email') IS NULL) AND (users.fb_uid = sqlc.narg('fb_uid') OR sqlc.narg('fb_uid') IS NULL) - AND (users.deleted_at IS NULL OR sqlc.arg('include_deleted')::boolean = TRUE); + AND (users.deleted_at IS NULL OR sqlc.arg('include_deleted')::boolean = TRUE) +LIMIT 1; -- name: ExistsUserByNickname :one SELECT CASE From 288476ba19e6a578bc378942441b578499b55f24 Mon Sep 17 00:00:00 2001 From: litsynp Date: Thu, 25 Apr 2024 03:01:19 +0900 Subject: [PATCH 06/12] refactor: streamline and use sqlc for pet --- .golangci.yml | 4 +- cmd/import_breeds/main.go | 36 +- cmd/server/handler/user_handler.go | 16 +- go.mod | 1 + go.sum | 2 + internal/common/null.go | 7 + internal/datatype/date.go | 51 +++ .../domain/{pet/breed.go => breed/model.go} | 19 +- internal/domain/breed/view.go | 33 ++ internal/domain/commonvo/vo.go | 8 + internal/domain/pet/model.go | 132 ++++++ internal/domain/pet/params.go | 39 ++ internal/domain/pet/pet.go | 60 --- internal/domain/pet/request.go | 38 +- internal/domain/pet/view.go | 109 ++--- internal/domain/sospost/view.go | 4 +- internal/domain/user/model.go | 4 +- internal/domain/user/view.go | 10 +- internal/infra/database/gen/pets.sql.go | 375 ++++++++++++++++++ internal/postgres/breed_store.go | 54 +-- internal/postgres/pet_store.go | 220 ---------- internal/service/breed_service.go | 20 +- internal/service/sos_post_service.go | 6 +- .../service/tests/sos_post_service_test.go | 98 +++-- internal/service/tests/user_service_test.go | 118 ++---- internal/service/user_service.go | 151 +++---- internal/tests/assert/pet.go | 63 +++ internal/tests/factories.go | 25 +- internal/tests/service.go | 16 +- queries/pets.sql | 113 ++++++ 30 files changed, 1220 insertions(+), 612 deletions(-) create mode 100644 internal/datatype/date.go rename internal/domain/{pet/breed.go => breed/model.go} (54%) create mode 100644 internal/domain/breed/view.go create mode 100644 internal/domain/commonvo/vo.go create mode 100644 internal/domain/pet/model.go create mode 100644 internal/domain/pet/params.go delete mode 100644 internal/domain/pet/pet.go create mode 100644 internal/infra/database/gen/pets.sql.go delete mode 100644 internal/postgres/pet_store.go create mode 100644 internal/tests/assert/pet.go create mode 100644 queries/pets.sql diff --git a/.golangci.yml b/.golangci.yml index ec8d76a5..151c3c83 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -486,7 +486,7 @@ linters: - asciicheck # checks that your code does not contain non-ASCII identifiers - bidichk # checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - - cyclop # checks function and package cyclomatic complexity + # - cyclop # checks function and package cyclomatic complexity - dupl # tool for code clone detection - durationcheck # checks for two durations multiplied together - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error @@ -511,7 +511,7 @@ linters: - gosec # inspects source code for security problems - grouper # analyzes expression groups - lll # reports long lines - - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - loggercheck # checks key value pairs for commonvo logger libraries (kitlog,klog,logr,zap) - makezero # finds slice declarations with non-zero initial length - mirror - nakedret # finds naked returns in functions greater than a specified function length diff --git a/cmd/import_breeds/main.go b/cmd/import_breeds/main.go index 9e47ec89..484f12fb 100644 --- a/cmd/import_breeds/main.go +++ b/cmd/import_breeds/main.go @@ -7,11 +7,13 @@ import ( "flag" "log" + "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" + "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + "github.com/pet-sitter/pets-next-door-api/cmd/import_breeds/breedsimporterservice" pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/configs" - "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" "github.com/pet-sitter/pets-next-door-api/internal/postgres" ) @@ -40,16 +42,16 @@ func main() { switch flags.petTypeToImport { case Cat: catRows := client.GetCatNames(spreadsheet) - importBreeds(ctx, db, pet.PetTypeCat, &catRows) + importBreeds(ctx, db, commonvo.PetTypeCat, &catRows) case Dog: dogRows := client.GetDogNames(spreadsheet) - importBreeds(ctx, db, pet.PetTypeDog, &dogRows) + importBreeds(ctx, db, commonvo.PetTypeDog, &dogRows) case All: catRows := client.GetCatNames(spreadsheet) dogRows := client.GetDogNames(spreadsheet) - importBreeds(ctx, db, pet.PetTypeCat, &catRows) - importBreeds(ctx, db, pet.PetTypeDog, &dogRows) + importBreeds(ctx, db, commonvo.PetTypeCat, &catRows) + importBreeds(ctx, db, commonvo.PetTypeDog, &dogRows) } log.Println("Completed importing pet types to database") @@ -90,11 +92,11 @@ func parseFlags() Flags { } func importBreed( - ctx context.Context, conn *database.DB, petType pet.PetType, row breedsimporterservice.Row, -) (*pet.Breed, *pnd.AppError) { + ctx context.Context, conn *database.DB, petType commonvo.PetType, row breedsimporterservice.Row, +) (*breed.Breed, *pnd.AppError) { log.Printf("Importing breed with pet_type: %s, name: %s to database", petType, row.Breed) - var breed *pet.Breed + var breedData *breed.Breed err := database.WithTransaction(ctx, conn, func(tx *database.Tx) *pnd.AppError { existing, err := postgres.FindBreedByPetTypeAndName(ctx, tx, petType, row.Breed) if err != nil && !errors.Is(err.Err, sql.ErrNoRows) { @@ -108,20 +110,20 @@ func importBreed( existing.PetType, existing.Name, ) - breed = existing + breedData = existing return nil } - breed, err = postgres.CreateBreed(ctx, tx, &pet.Breed{PetType: petType, Name: row.Breed}) + breedData, err = postgres.CreateBreed(ctx, tx, &breed.Breed{PetType: petType, Name: row.Breed}) if err != nil { return err } log.Printf( "Succeeded to import breed with id: %d, pet_type: %s, name: %s to database", - breed.ID, - breed.PetType, - breed.Name, + breedData.ID, + breedData.PetType, + breedData.Name, ) return nil }) @@ -129,14 +131,14 @@ func importBreed( return nil, err } - return breed, nil + return breedData, nil } -func importBreeds(ctx context.Context, conn *database.DB, petType pet.PetType, rows *[]breedsimporterservice.Row) { +func importBreeds(ctx context.Context, conn *database.DB, petType commonvo.PetType, rows *[]breedsimporterservice.Row) { for _, row := range *rows { - breed, err := importBreed(ctx, conn, petType, row) + breedData, err := importBreed(ctx, conn, petType, row) if err != nil { - log.Printf("Failed to import breed with pet_type: %s, name: %s to database", breed.PetType, breed.Name) + log.Printf("Failed to import breed with pet_type: %s, name: %s to database", breedData.PetType, breedData.Name) } } } diff --git a/cmd/server/handler/user_handler.go b/cmd/server/handler/user_handler.go index f9381aed..b747c147 100644 --- a/cmd/server/handler/user_handler.go +++ b/cmd/server/handler/user_handler.go @@ -237,7 +237,7 @@ func (h *UserHandler) AddMyPets(c echo.Context) error { // @Tags users,pets // @Produce json // @Security FirebaseAuth -// @Success 200 {object} pet.FindMyPetsView +// @Success 200 {object} pet.ListView // @Router /users/me/pets [get] func (h *UserHandler) FindMyPets(c echo.Context) error { foundUser, err := h.authService.VerifyAuthAndGetUser(c.Request().Context(), c.Request().Header.Get("Authorization")) @@ -245,9 +245,11 @@ func (h *UserHandler) FindMyPets(c echo.Context) error { return c.JSON(err.StatusCode, err) } - uid := foundUser.FirebaseUID - - res, err := h.userService.FindPetsByOwnerUID(c.Request().Context(), uid) + res, err := h.userService.FindPets(c.Request().Context(), pet.FindPetsParams{ + Page: 1, + Size: 100, + OwnerID: &foundUser.ID, + }) if err != nil { return c.JSON(err.StatusCode, err) } @@ -264,7 +266,7 @@ func (h *UserHandler) FindMyPets(c echo.Context) error { // @Security FirebaseAuth // @Param petID path int true "반려동물 ID" // @Param request body pet.UpdatePetRequest true "반려동물 수정 요청" -// @Success 200 {object} pet.PetView +// @Success 200 {object} pet.DetailView // @Router /users/me/pets/{petID} [put] func (h *UserHandler) UpdateMyPet(c echo.Context) error { foundUser, err := h.authService.VerifyAuthAndGetUser(c.Request().Context(), c.Request().Header.Get("Authorization")) @@ -283,7 +285,7 @@ func (h *UserHandler) UpdateMyPet(c echo.Context) error { return c.JSON(err.StatusCode, err) } - res, err := h.userService.UpdatePet(c.Request().Context(), uid, *petID, updatePetRequest) + res, err := h.userService.UpdatePet(c.Request().Context(), uid, int64(*petID), updatePetRequest) if err != nil { return c.JSON(err.StatusCode, err) } @@ -311,7 +313,7 @@ func (h *UserHandler) DeleteMyPet(c echo.Context) error { return c.JSON(err.StatusCode, err) } - if err := h.userService.DeletePet(c.Request().Context(), uid, *petID); err != nil { + if err := h.userService.DeletePet(c.Request().Context(), uid, int64(*petID)); err != nil { return c.JSON(err.StatusCode, err) } diff --git a/go.mod b/go.mod index e0836d65..e320e4fa 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/labstack/echo/v4 v4.11.4 github.com/rs/zerolog v1.32.0 + github.com/shopspring/decimal v1.2.0 github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.16.1 google.golang.org/api v0.126.0 diff --git a/go.sum b/go.sum index a61edb3f..b6d97d47 100644 --- a/go.sum +++ b/go.sum @@ -188,6 +188,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/common/null.go b/internal/common/null.go index f763d22e..f588e449 100644 --- a/internal/common/null.go +++ b/internal/common/null.go @@ -69,3 +69,10 @@ func IntPtrToNullInt32(val *int) sql.NullInt32 { Valid: IsNotNil(val), } } + +func Int64PtrToNullInt32(val *int64) sql.NullInt32 { + return sql.NullInt32{ + Int32: int32(DerefOrEmpty(val)), + Valid: IsNotNil(val), + } +} diff --git a/internal/datatype/date.go b/internal/datatype/date.go new file mode 100644 index 00000000..444b13f6 --- /dev/null +++ b/internal/datatype/date.go @@ -0,0 +1,51 @@ +package datatype + +import ( + "database/sql" + "database/sql/driver" + "time" + + "cloud.google.com/go/civil" +) + +type Date civil.Date + +func (date *Date) Scan(value interface{}) (err error) { + nullTime := &sql.NullTime{} + err = nullTime.Scan(value) + *date = Date(civil.DateOf(nullTime.Time)) + return err +} + +func (date Date) Value() (driver.Value, error) { + return time.Date(date.Year, date.Month, date.Day, 0, 0, 0, 0, time.UTC), nil +} + +func (date Date) MarshalJSON() ([]byte, error) { + marshalled := make([]byte, 0) + text, err := civil.Date(date).MarshalText() + marshalled = append(marshalled, byte('"')) + marshalled = append(marshalled, text...) + marshalled = append(marshalled, byte('"')) + return marshalled, err +} + +func (date *Date) UnmarshalJSON(b []byte) error { + c := civil.Date{} + err := c.UnmarshalText(b[1 : len(b)-1]) + *date = Date(c) + return err +} + +func DateOf(t time.Time) Date { + return Date(civil.DateOf(t)) +} + +func ParseDate(s string) (Date, error) { + c, err := civil.ParseDate(s) + return Date(c), err +} + +func (date Date) String() string { + return civil.Date(date).String() +} diff --git a/internal/domain/pet/breed.go b/internal/domain/breed/model.go similarity index 54% rename from internal/domain/pet/breed.go rename to internal/domain/breed/model.go index 4dfcd671..922223ea 100644 --- a/internal/domain/pet/breed.go +++ b/internal/domain/breed/model.go @@ -1,20 +1,21 @@ -package pet +package breed import ( "context" + "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" pnd "github.com/pet-sitter/pets-next-door-api/api" ) type Breed struct { - ID int `field:"id"` - Name string `field:"name"` - PetType PetType `field:"pet_type"` - CreatedAt string `field:"created_at"` - UpdatedAt string `field:"updated_at"` - DeletedAt string `field:"deleted_at"` + ID int `field:"id"` + Name string `field:"name"` + PetType commonvo.PetType `field:"pet_type"` + CreatedAt string `field:"created_at"` + UpdatedAt string `field:"updated_at"` + DeletedAt string `field:"deleted_at"` } type BreedList struct { @@ -29,6 +30,8 @@ func NewBreedList(page, size int) *BreedList { type BreedStore interface { FindBreeds(ctx context.Context, tx *database.Tx, page, size int, petType *string) (*BreedList, *pnd.AppError) - FindBreedByPetTypeAndName(ctx context.Context, tx *database.Tx, petType PetType, name string) (*Breed, *pnd.AppError) + FindBreedByPetTypeAndName( + ctx context.Context, tx *database.Tx, petType commonvo.PetType, name string, + ) (*Breed, *pnd.AppError) CreateBreed(ctx context.Context, tx *database.Tx, breed *Breed) (*Breed, *pnd.AppError) } diff --git a/internal/domain/breed/view.go b/internal/domain/breed/view.go new file mode 100644 index 00000000..448c6d6c --- /dev/null +++ b/internal/domain/breed/view.go @@ -0,0 +1,33 @@ +package breed + +import ( + pnd "github.com/pet-sitter/pets-next-door-api/api" + "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" +) + +type BreedView struct { + ID int `json:"id"` + PetType commonvo.PetType `json:"petType"` + Name string `json:"name"` +} + +type BreedListView struct { + *pnd.PaginatedView[*BreedView] +} + +func (breeds *BreedList) ToBreedListView() *BreedListView { + breedViews := make([]*BreedView, len(breeds.Items)) + for i, breed := range breeds.Items { + breedViews[i] = &BreedView{ + ID: breed.ID, + PetType: breed.PetType, + Name: breed.Name, + } + } + + return &BreedListView{ + PaginatedView: pnd.NewPaginatedView( + breeds.Page, breeds.Size, breeds.IsLastPage, breedViews, + ), + } +} diff --git a/internal/domain/commonvo/vo.go b/internal/domain/commonvo/vo.go new file mode 100644 index 00000000..b45c17e0 --- /dev/null +++ b/internal/domain/commonvo/vo.go @@ -0,0 +1,8 @@ +package commonvo + +type PetType string + +const ( + PetTypeDog PetType = "dog" + PetTypeCat PetType = "cat" +) diff --git a/internal/domain/pet/model.go b/internal/domain/pet/model.go new file mode 100644 index 00000000..d97dc5b7 --- /dev/null +++ b/internal/domain/pet/model.go @@ -0,0 +1,132 @@ +package pet + +import ( + "database/sql" + "time" + + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + "github.com/pet-sitter/pets-next-door-api/internal/datatype" + "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" + "github.com/shopspring/decimal" +) + +type BasePet struct { + ID int `field:"id" json:"id"` + OwnerID int `field:"owner_id" json:"owner_id"` + Name string `field:"name" json:"name"` + PetType commonvo.PetType `field:"pet_type" json:"pet_type"` + Sex PetSex `field:"sex" json:"sex"` + Neutered bool `field:"neutered" json:"neutered"` + Breed string `field:"breed" json:"breed"` + BirthDate datatype.Date `field:"birth_date" json:"birth_date"` + WeightInKg decimal.Decimal `field:"weight_in_kg" json:"weight_in_kg"` + Remarks string `field:"remarks" json:"remarks"` + CreatedAt string `field:"created_at" json:"created_at"` + UpdatedAt string `field:"updated_at" json:"updated_at"` + DeletedAt string `field:"deleted_at" json:"deleted_at"` +} + +type Pet struct { + BasePet + ProfileImageID *int `field:"profile_image_id"` +} + +type PetList []*Pet + +type PetWithProfileImage struct { + BasePet + ProfileImageURL *string `field:"profile_image_url" json:"profile_image_url"` +} + +type PetWithProfileList []*PetWithProfileImage + +type PetSex string + +const ( + PetSexMale PetSex = "male" + PetSexFemale PetSex = "female" +) + +type WithProfileImage struct { + ID int64 + OwnerID int64 + Name string + PetType commonvo.PetType + Sex PetSex + Neutered bool + Breed string + BirthDate datatype.Date + WeightInKg decimal.Decimal + Remarks string + ProfileImageURL *string + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +func ToWithProfileImage(row databasegen.FindPetRow) *WithProfileImage { + weightInKg, _ := decimal.NewFromString(row.WeightInKg) + birthDate := datatype.DateOf(row.BirthDate) + + return &WithProfileImage{ + ID: int64(row.ID), + OwnerID: row.OwnerID, + Name: row.Name, + PetType: commonvo.PetType(row.PetType), + Sex: PetSex(row.Sex), + Neutered: row.Neutered, + Breed: row.Breed, + BirthDate: birthDate, + WeightInKg: weightInKg, + Remarks: row.Remarks, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + DeletedAt: row.DeletedAt, + } +} + +func ToWithProfileImageFromRows(row databasegen.FindPetsRow) *WithProfileImage { + weightInKg, _ := decimal.NewFromString(row.WeightInKg) + birthDate := datatype.DateOf(row.BirthDate) + + return &WithProfileImage{ + ID: int64(row.ID), + OwnerID: row.OwnerID, + Name: row.Name, + PetType: commonvo.PetType(row.PetType), + Sex: PetSex(row.Sex), + Neutered: row.Neutered, + Breed: row.Breed, + BirthDate: birthDate, + WeightInKg: weightInKg, + Remarks: row.Remarks, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + DeletedAt: row.DeletedAt, + } +} + +func ToWithProfileImageFromIDsRows(row databasegen.FindPetsByIDsRow) *WithProfileImage { + weightInKg, _ := decimal.NewFromString(row.WeightInKg) + birthDate := datatype.DateOf(row.BirthDate) + + return &WithProfileImage{ + ID: int64(row.ID), + OwnerID: row.OwnerID, + Name: row.Name, + PetType: commonvo.PetType(row.PetType), + Sex: PetSex(row.Sex), + Neutered: row.Neutered, + Breed: row.Breed, + BirthDate: birthDate, + WeightInKg: weightInKg, + Remarks: row.Remarks, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + DeletedAt: row.DeletedAt, + } +} diff --git a/internal/domain/pet/params.go b/internal/domain/pet/params.go new file mode 100644 index 00000000..b0dba059 --- /dev/null +++ b/internal/domain/pet/params.go @@ -0,0 +1,39 @@ +package pet + +import ( + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" +) + +type FindPetParams struct { + ID *int64 + OwnerID *int64 + IncludeDeleted bool +} + +func (p *FindPetParams) ToDBParams() databasegen.FindPetParams { + return databasegen.FindPetParams{ + ID: utils.Int64PtrToNullInt32(p.ID), + OwnerID: utils.Int64PtrToNullInt64(p.OwnerID), + IncludeDeleted: p.IncludeDeleted, + } +} + +type FindPetsParams struct { + Page int + Size int + ID *int64 + OwnerID *int64 + IncludeDeleted bool +} + +func (p *FindPetsParams) ToDBParams() databasegen.FindPetsParams { + pagination := utils.OffsetAndLimit(p.Page, p.Size) + return databasegen.FindPetsParams{ + Limit: int32(pagination.Limit), + Offset: int32(pagination.Offset), + ID: utils.Int64PtrToNullInt32(p.ID), + OwnerID: utils.Int64PtrToNullInt64(p.OwnerID), + IncludeDeleted: p.IncludeDeleted, + } +} diff --git a/internal/domain/pet/pet.go b/internal/domain/pet/pet.go deleted file mode 100644 index 9fda18b3..00000000 --- a/internal/domain/pet/pet.go +++ /dev/null @@ -1,60 +0,0 @@ -package pet - -import ( - "context" - - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - - pnd "github.com/pet-sitter/pets-next-door-api/api" -) - -type BasePet struct { - ID int `field:"id" json:"id"` - OwnerID int `field:"owner_id" json:"owner_id"` - Name string `field:"name" json:"name"` - PetType PetType `field:"pet_type" json:"pet_type"` - Sex PetSex `field:"sex" json:"sex"` - Neutered bool `field:"neutered" json:"neutered"` - Breed string `field:"breed" json:"breed"` - BirthDate string `field:"birth_date" json:"birth_date"` - WeightInKg float64 `field:"weight_in_kg" json:"weight_in_kg"` - Remarks string `field:"remarks" json:"remarks"` - CreatedAt string `field:"created_at" json:"created_at"` - UpdatedAt string `field:"updated_at" json:"updated_at"` - DeletedAt string `field:"deleted_at" json:"deleted_at"` -} - -type Pet struct { - BasePet - ProfileImageID *int `field:"profile_image_id"` -} - -type PetList []*Pet - -type PetWithProfileImage struct { - BasePet - ProfileImageURL *string `field:"profile_image_url" json:"profile_image_url"` -} - -type PetWithProfileList []*PetWithProfileImage - -type PetStore interface { - CreatePet(ctx context.Context, tx *database.Tx, pet *Pet) (*PetWithProfileImage, *pnd.AppError) - FindPetByID(ctx context.Context, tx *database.Tx, petID int) (*PetWithProfileImage, *pnd.AppError) - FindPetsByOwnerID(ctx context.Context, tx *database.Tx, ownerID int) (*PetWithProfileList, *pnd.AppError) - UpdatePet(ctx context.Context, tx *database.Tx, updatePetRequest *UpdatePetRequest) *pnd.AppError -} - -type PetType string - -const ( - PetTypeDog PetType = "dog" - PetTypeCat PetType = "cat" -) - -type PetSex string - -const ( - PetSexMale PetSex = "male" - PetSexFemale PetSex = "female" -) diff --git a/internal/domain/pet/request.go b/internal/domain/pet/request.go index acd66db0..2b022e40 100644 --- a/internal/domain/pet/request.go +++ b/internal/domain/pet/request.go @@ -1,19 +1,25 @@ package pet +import ( + utils "github.com/pet-sitter/pets-next-door-api/internal/datatype" + "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + "github.com/shopspring/decimal" +) + type AddPetsToOwnerRequest struct { Pets []AddPetRequest `json:"pets" validate:"required"` } type AddPetRequest struct { - Name string `json:"name" validate:"required"` - PetType PetType `json:"petType" validate:"required,oneof=dog cat"` - Sex PetSex `json:"sex" validate:"required,oneof=male female"` - Neutered bool `json:"neutered" validate:"required"` - Breed string `json:"breed" validate:"required"` - BirthDate string `json:"birthDate" validate:"required"` - WeightInKg float64 `json:"weightInKg" validate:"required"` - Remarks string `json:"remarks"` - ProfileImageID *int `json:"profileImageId"` + Name string `json:"name" validate:"required"` + PetType commonvo.PetType `json:"petType" validate:"required,oneof=dog cat"` + Sex PetSex `json:"sex" validate:"required,oneof=male female"` + Neutered bool `json:"neutered" validate:"required"` + Breed string `json:"breed" validate:"required"` + BirthDate utils.Date `json:"birthDate" validate:"required"` + WeightInKg decimal.Decimal `json:"weightInKg" validate:"required"` + Remarks string `json:"remarks"` + ProfileImageID *int `json:"profileImageId"` } func (r *AddPetRequest) ToBasePet(ownerID int) *BasePet { @@ -38,11 +44,11 @@ func (r *AddPetRequest) ToPet(ownerID int) *Pet { } type UpdatePetRequest struct { - Name string `json:"name" validate:"required"` - Neutered bool `json:"neutered" validate:"required"` - Breed string `json:"breed" validate:"required"` - BirthDate string `json:"birthDate" validate:"required"` - WeightInKg float64 `json:"weightInKg" validate:"required"` - Remarks string `json:"remarks"` - ProfileImageID *int `json:"profileImageId"` + Name string `json:"name" validate:"required"` + Neutered bool `json:"neutered" validate:"required"` + Breed string `json:"breed" validate:"required"` + BirthDate utils.Date `json:"birthDate" validate:"required"` + WeightInKg decimal.Decimal `json:"weightInKg" validate:"required"` + Remarks string `json:"remarks"` + ProfileImageID *int `json:"profileImageId"` } diff --git a/internal/domain/pet/view.go b/internal/domain/pet/view.go index 02b84efc..5af31b04 100644 --- a/internal/domain/pet/view.go +++ b/internal/domain/pet/view.go @@ -1,33 +1,23 @@ package pet import ( - pnd "github.com/pet-sitter/pets-next-door-api/api" - utils "github.com/pet-sitter/pets-next-door-api/internal/common" + utils "github.com/pet-sitter/pets-next-door-api/internal/datatype" + "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" + "github.com/shopspring/decimal" ) -type FindMyPetsView struct { - Pets []PetView `json:"pets"` -} - -func (pets *PetWithProfileList) ToFindMyPetsView() *FindMyPetsView { - petViews := make([]PetView, len(*pets)) - for i, pet := range *pets { - petViews[i] = *pet.ToPetView() - } - return &FindMyPetsView{Pets: petViews} -} - type PetView struct { - ID int `json:"id"` - Name string `json:"name"` - PetType PetType `json:"petType"` - Sex PetSex `json:"sex"` - Neutered bool `json:"neutered"` - Breed string `json:"breed"` - BirthDate string `json:"birthDate"` - WeightInKg float64 `json:"weightInKg"` - Remarks string `json:"remarks"` - ProfileImageURL *string `json:"profileImageUrl"` + ID int `json:"id"` + Name string `json:"name"` + PetType commonvo.PetType `json:"petType"` + Sex PetSex `json:"sex"` + Neutered bool `json:"neutered"` + Breed string `json:"breed"` + BirthDate utils.Date `json:"birthDate"` + WeightInKg decimal.Decimal `json:"weightInKg"` + Remarks string `json:"remarks"` + ProfileImageURL *string `json:"profileImageUrl"` } func (pet *Pet) ToPetView() *PetView { @@ -38,7 +28,7 @@ func (pet *Pet) ToPetView() *PetView { Sex: pet.Sex, Neutered: pet.Neutered, Breed: pet.Breed, - BirthDate: utils.FormatDate(pet.BirthDate), + BirthDate: pet.BirthDate, WeightInKg: pet.WeightInKg, Remarks: pet.Remarks, } @@ -60,44 +50,65 @@ func (pet *PetWithProfileImage) ToPetView() *PetView { Sex: pet.Sex, Neutered: pet.Neutered, Breed: pet.Breed, - BirthDate: utils.FormatDate(pet.BirthDate), + BirthDate: pet.BirthDate, WeightInKg: pet.WeightInKg, Remarks: pet.Remarks, ProfileImageURL: pet.ProfileImageURL, } } -func (pets *PetWithProfileList) ToPetViewList() []PetView { - petViews := make([]PetView, len(*pets)) - for i, pet := range *pets { - petViews[i] = *pet.ToPetView() +type DetailView struct { + ID int64 `json:"id"` + Name string `json:"name"` + PetType commonvo.PetType `json:"petType"` + Sex PetSex `json:"sex"` + Neutered bool `json:"neutered"` + Breed string `json:"breed"` + BirthDate utils.Date `json:"birthDate"` + WeightInKg decimal.Decimal `json:"weightInKg"` + Remarks string `json:"remarks"` + ProfileImageURL *string `json:"profileImageUrl"` +} + +func (pet *WithProfileImage) ToDetailView() *DetailView { + return &DetailView{ + ID: pet.ID, + Name: pet.Name, + PetType: pet.PetType, + Sex: pet.Sex, + Neutered: pet.Neutered, + Breed: pet.Breed, + BirthDate: pet.BirthDate, + WeightInKg: pet.WeightInKg, + Remarks: pet.Remarks, + ProfileImageURL: pet.ProfileImageURL, } - return petViews } -type BreedView struct { - ID int `json:"id"` - PetType PetType `json:"petType"` - Name string `json:"name"` +type ListView struct { + Pets []DetailView `json:"pets"` } -type BreedListView struct { - *pnd.PaginatedView[*BreedView] +func ToListView(rows []databasegen.FindPetsRow) *ListView { + pl := &ListView{Pets: make([]DetailView, len(rows))} + for i, row := range rows { + pl.Pets[i] = *ToWithProfileImageFromRows(row).ToDetailView() + } + return pl } -func (breeds *BreedList) ToBreedListView() *BreedListView { - breedViews := make([]*BreedView, len(breeds.Items)) - for i, breed := range breeds.Items { - breedViews[i] = &BreedView{ - ID: breed.ID, - PetType: breed.PetType, - Name: breed.Name, - } +func ToListViewFromIDsRows(rows []databasegen.FindPetsByIDsRow) *ListView { + pl := &ListView{Pets: make([]DetailView, len(rows))} + for i, row := range rows { + pl.Pets[i] = *ToWithProfileImageFromIDsRows(row).ToDetailView() } + return pl +} - return &BreedListView{ - PaginatedView: pnd.NewPaginatedView( - breeds.Page, breeds.Size, breeds.IsLastPage, breedViews, - ), +func (pets *PetWithProfileList) ToPetViewList() []PetView { + petViews := make([]PetView, len(*pets)) + for i, pet := range *pets { + petViews[i] = *pet.ToPetView() } + return petViews } diff --git a/internal/domain/sospost/view.go b/internal/domain/sospost/view.go index 4cfacced..b960517d 100644 --- a/internal/domain/sospost/view.go +++ b/internal/domain/sospost/view.go @@ -33,14 +33,14 @@ func (cl *ConditionList) ToConditionViewList() []ConditionView { type WriteSOSPostRequest struct { Title string `json:"title" validate:"required"` Content string `json:"content" validate:"required"` - ImageIDs []int `json:"imageIds" validate:"required"` + ImageIDs []int64 `json:"imageIds" validate:"required"` Reward string `json:"reward"` Dates []SOSDateView `json:"dates" validate:"required"` CareType CareType `json:"careType" validate:"required,oneof=foster visiting"` CarerGender CarerGender `json:"carerGender" validate:"required,oneof=male female all"` RewardType RewardType `json:"rewardType" validate:"required,oneof=fee gifticon negotiable"` ConditionIDs []int `json:"conditionIds"` - PetIDs []int `json:"petIds"` + PetIDs []int64 `json:"petIds"` } type WriteSOSPostView struct { diff --git a/internal/domain/user/model.go b/internal/domain/user/model.go index 76c301d5..a5cfe793 100644 --- a/internal/domain/user/model.go +++ b/internal/domain/user/model.go @@ -26,7 +26,7 @@ func (f FirebaseProviderType) NullString() sql.NullString { } type WithProfileImage struct { - ID int + ID int64 Email string Password string Nickname string @@ -41,7 +41,7 @@ type WithProfileImage struct { func ToWithProfileImage(row databasegen.FindUserRow) *WithProfileImage { return &WithProfileImage{ - ID: int(row.ID), + ID: int64(row.ID), Email: row.Email, Nickname: row.Nickname, Fullname: row.Fullname, diff --git a/internal/domain/user/view.go b/internal/domain/user/view.go index 6761f6cc..443f342a 100644 --- a/internal/domain/user/view.go +++ b/internal/domain/user/view.go @@ -7,7 +7,7 @@ import ( ) type InternalView struct { - ID int `json:"id"` + ID int64 `json:"id"` Email string `json:"email"` Nickname string `json:"nickname"` Fullname string `json:"fullname"` @@ -28,7 +28,7 @@ func (r *InternalView) ToMyProfileView() *MyProfileView { } type MyProfileView struct { - ID int `json:"id"` + ID int64 `json:"id"` Email string `json:"email"` Nickname string `json:"nickname"` Fullname string `json:"fullname"` @@ -60,14 +60,14 @@ func NewStatusView(providerType FirebaseProviderType) *StatusView { } type WithoutPrivateInfo struct { - ID int `json:"id"` + ID int64 `json:"id"` Nickname string `json:"nickname"` ProfileImageURL *string `json:"profileImageUrl"` } func ToWithoutPrivateInfo(row databasegen.FindUserRow) *WithoutPrivateInfo { return &WithoutPrivateInfo{ - ID: int(row.ID), + ID: int64(row.ID), Nickname: row.Nickname, ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), } @@ -84,7 +84,7 @@ func ToListWithoutPrivateInfo(page, size int, rows []databasegen.FindUsersRow) * for _, row := range rows { ul.Items = append(ul.Items, WithoutPrivateInfo{ - ID: int(row.ID), + ID: int64(row.ID), Nickname: row.Nickname, ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), }) diff --git a/internal/infra/database/gen/pets.sql.go b/internal/infra/database/gen/pets.sql.go new file mode 100644 index 00000000..43036814 --- /dev/null +++ b/internal/infra/database/gen/pets.sql.go @@ -0,0 +1,375 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: pets.sql + +package databasegen + +import ( + "context" + "database/sql" + "time" + + "github.com/lib/pq" +) + +const createPet = `-- name: CreatePet :one +INSERT INTO pets +(owner_id, + name, + pet_type, + sex, + neutered, + breed, + birth_date, + weight_in_kg, + remarks, + profile_image_id, + created_at, + updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW()) +RETURNING id, created_at, updated_at +` + +type CreatePetParams struct { + OwnerID int64 + Name string + PetType string + Sex string + Neutered bool + Breed string + BirthDate time.Time + WeightInKg string + Remarks string + ProfileImageID sql.NullInt64 +} + +type CreatePetRow struct { + ID int32 + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) CreatePet(ctx context.Context, arg CreatePetParams) (CreatePetRow, error) { + row := q.db.QueryRowContext(ctx, createPet, + arg.OwnerID, + arg.Name, + arg.PetType, + arg.Sex, + arg.Neutered, + arg.Breed, + arg.BirthDate, + arg.WeightInKg, + arg.Remarks, + arg.ProfileImageID, + ) + var i CreatePetRow + err := row.Scan(&i.ID, &i.CreatedAt, &i.UpdatedAt) + return i, err +} + +const deletePet = `-- name: DeletePet :exec +UPDATE + pets +SET deleted_at = NOW() +WHERE id = $1 +` + +func (q *Queries) DeletePet(ctx context.Context, id int32) error { + _, err := q.db.ExecContext(ctx, deletePet, id) + return err +} + +const findPet = `-- name: FindPet :one +SELECT pets.id, + pets.owner_id, + pets.name, + pets.pet_type, + pets.sex, + pets.neutered, + pets.breed, + pets.birth_date, + pets.weight_in_kg, + pets.remarks, + media.url AS profile_image_url, + pets.created_at, + pets.updated_at, + pets.deleted_at +FROM pets + LEFT OUTER JOIN + media + ON + pets.profile_image_id = media.id +WHERE (pets.id = $1 OR $1 IS NULL) + AND (pets.owner_id = $2 OR $2 IS NULL) + AND ($3::boolean = TRUE OR + ($3::boolean = FALSE AND pets.deleted_at IS NULL)) +LIMIT 1 +` + +type FindPetParams struct { + ID sql.NullInt32 + OwnerID sql.NullInt64 + IncludeDeleted bool +} + +type FindPetRow struct { + ID int32 + OwnerID int64 + Name string + PetType string + Sex string + Neutered bool + Breed string + BirthDate time.Time + WeightInKg string + Remarks string + ProfileImageUrl sql.NullString + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +func (q *Queries) FindPet(ctx context.Context, arg FindPetParams) (FindPetRow, error) { + row := q.db.QueryRowContext(ctx, findPet, arg.ID, arg.OwnerID, arg.IncludeDeleted) + var i FindPetRow + err := row.Scan( + &i.ID, + &i.OwnerID, + &i.Name, + &i.PetType, + &i.Sex, + &i.Neutered, + &i.Breed, + &i.BirthDate, + &i.WeightInKg, + &i.Remarks, + &i.ProfileImageUrl, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const findPets = `-- name: FindPets :many +SELECT pets.id, + pets.owner_id, + pets.name, + pets.pet_type, + pets.sex, + pets.neutered, + pets.breed, + pets.birth_date, + pets.weight_in_kg, + pets.remarks, + media.url AS profile_image_url, + pets.created_at, + pets.updated_at, + pets.deleted_at +FROM pets + LEFT OUTER JOIN + media + ON + pets.profile_image_id = media.id +WHERE (pets.id = $3 OR $3 IS NULL) + AND (pets.owner_id = $4 OR $4 IS NULL) + AND ($5::boolean = TRUE OR + ($5::boolean = FALSE AND pets.deleted_at IS NULL)) +ORDER BY pets.created_at DESC +LIMIT $1 OFFSET $2 +` + +type FindPetsParams struct { + Limit int32 + Offset int32 + ID sql.NullInt32 + OwnerID sql.NullInt64 + IncludeDeleted bool +} + +type FindPetsRow struct { + ID int32 + OwnerID int64 + Name string + PetType string + Sex string + Neutered bool + Breed string + BirthDate time.Time + WeightInKg string + Remarks string + ProfileImageUrl sql.NullString + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +func (q *Queries) FindPets(ctx context.Context, arg FindPetsParams) ([]FindPetsRow, error) { + rows, err := q.db.QueryContext(ctx, findPets, + arg.Limit, + arg.Offset, + arg.ID, + arg.OwnerID, + arg.IncludeDeleted, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindPetsRow + for rows.Next() { + var i FindPetsRow + if err := rows.Scan( + &i.ID, + &i.OwnerID, + &i.Name, + &i.PetType, + &i.Sex, + &i.Neutered, + &i.Breed, + &i.BirthDate, + &i.WeightInKg, + &i.Remarks, + &i.ProfileImageUrl, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); 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 +} + +const findPetsByIDs = `-- name: FindPetsByIDs :many +SELECT pets.id, + pets.owner_id, + pets.name, + pets.pet_type, + pets.sex, + pets.neutered, + pets.breed, + pets.birth_date, + pets.weight_in_kg, + pets.remarks, + media.url AS profile_image_url, + pets.created_at, + pets.updated_at, + pets.deleted_at +FROM pets + LEFT OUTER JOIN + media + ON + pets.profile_image_id = media.id +WHERE pets.id = ANY ($1::int[]) + AND ($2::boolean = TRUE OR + ($2::boolean = FALSE AND pets.deleted_at IS NULL)) +ORDER BY pets.created_at DESC +` + +type FindPetsByIDsParams struct { + Ids []int32 + IncludeDeleted bool +} + +type FindPetsByIDsRow struct { + ID int32 + OwnerID int64 + Name string + PetType string + Sex string + Neutered bool + Breed string + BirthDate time.Time + WeightInKg string + Remarks string + ProfileImageUrl sql.NullString + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +func (q *Queries) FindPetsByIDs(ctx context.Context, arg FindPetsByIDsParams) ([]FindPetsByIDsRow, error) { + rows, err := q.db.QueryContext(ctx, findPetsByIDs, pq.Array(arg.Ids), arg.IncludeDeleted) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindPetsByIDsRow + for rows.Next() { + var i FindPetsByIDsRow + if err := rows.Scan( + &i.ID, + &i.OwnerID, + &i.Name, + &i.PetType, + &i.Sex, + &i.Neutered, + &i.Breed, + &i.BirthDate, + &i.WeightInKg, + &i.Remarks, + &i.ProfileImageUrl, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); 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 +} + +const updatePet = `-- name: UpdatePet :exec +UPDATE + pets +SET name = $2, + neutered = $3, + breed = $4, + birth_date = $5, + weight_in_kg = $6, + remarks = $7, + profile_image_id = $8, + updated_at = NOW() +WHERE id = $1 +` + +type UpdatePetParams struct { + ID int32 + Name string + Neutered bool + Breed string + BirthDate time.Time + WeightInKg string + Remarks string + ProfileImageID sql.NullInt64 +} + +func (q *Queries) UpdatePet(ctx context.Context, arg UpdatePetParams) error { + _, err := q.db.ExecContext(ctx, updatePet, + arg.ID, + arg.Name, + arg.Neutered, + arg.Breed, + arg.BirthDate, + arg.WeightInKg, + arg.Remarks, + arg.ProfileImageID, + ) + return err +} diff --git a/internal/postgres/breed_store.go b/internal/postgres/breed_store.go index 5bcb2b44..c58529ae 100644 --- a/internal/postgres/breed_store.go +++ b/internal/postgres/breed_store.go @@ -3,12 +3,16 @@ package postgres import ( "context" + "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" + "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" ) -func FindBreeds(ctx context.Context, tx *database.Tx, page, size int, petType *string) (*pet.BreedList, *pnd.AppError) { +func FindBreeds( + ctx context.Context, tx *database.Tx, page, size int, petType *string) (*breed.BreedList, *pnd.AppError, +) { const sql = ` SELECT id, @@ -26,7 +30,7 @@ func FindBreeds(ctx context.Context, tx *database.Tx, page, size int, petType *s OFFSET $3 ` - breedList := pet.NewBreedList(page, size) + breedList := breed.NewBreedList(page, size) rows, err := tx.QueryContext(ctx, sql, petType, size+1, (page-1)*size) if err != nil { return nil, pnd.FromPostgresError(err) @@ -34,11 +38,13 @@ func FindBreeds(ctx context.Context, tx *database.Tx, page, size int, petType *s defer rows.Close() for rows.Next() { - breed := &pet.Breed{} - if err := rows.Scan(&breed.ID, &breed.Name, &breed.PetType, &breed.CreatedAt, &breed.UpdatedAt); err != nil { + breedData := &breed.Breed{} + if err := rows.Scan( + &breedData.ID, &breedData.Name, &breedData.PetType, &breedData.CreatedAt, &breedData.UpdatedAt, + ); err != nil { return nil, pnd.FromPostgresError(err) } - breedList.Items = append(breedList.Items, *breed) + breedList.Items = append(breedList.Items, *breedData) } if err := rows.Err(); err != nil { return nil, pnd.FromPostgresError(err) @@ -49,8 +55,8 @@ func FindBreeds(ctx context.Context, tx *database.Tx, page, size int, petType *s } func FindBreedByPetTypeAndName( - ctx context.Context, tx *database.Tx, petType pet.PetType, name string, -) (*pet.Breed, *pnd.AppError) { + ctx context.Context, tx *database.Tx, petType commonvo.PetType, name string, +) (*breed.Breed, *pnd.AppError) { const sql = ` SELECT id, @@ -66,24 +72,24 @@ func FindBreedByPetTypeAndName( deleted_at IS NULL ` - breed := &pet.Breed{} + breedData := &breed.Breed{} if err := tx.QueryRowContext(ctx, sql, petType, name, ).Scan( - &breed.ID, - &breed.Name, - &breed.PetType, - &breed.CreatedAt, - &breed.UpdatedAt, + &breedData.ID, + &breedData.Name, + &breedData.PetType, + &breedData.CreatedAt, + &breedData.UpdatedAt, ); err != nil { return nil, pnd.FromPostgresError(err) } - return breed, nil + return breedData, nil } -func CreateBreed(ctx context.Context, tx *database.Tx, breed *pet.Breed) (*pet.Breed, *pnd.AppError) { +func CreateBreed(ctx context.Context, tx *database.Tx, breedData *breed.Breed) (*breed.Breed, *pnd.AppError) { const sql = ` INSERT INTO breeds @@ -101,17 +107,17 @@ func CreateBreed(ctx context.Context, tx *database.Tx, breed *pet.Breed) (*pet.B ` if err := tx.QueryRowContext(ctx, sql, - breed.Name, - breed.PetType, + breedData.Name, + breedData.PetType, ).Scan( - &breed.ID, - &breed.PetType, - &breed.Name, - &breed.CreatedAt, - &breed.UpdatedAt, + &breedData.ID, + &breedData.PetType, + &breedData.Name, + &breedData.CreatedAt, + &breedData.UpdatedAt, ); err != nil { return nil, pnd.FromPostgresError(err) } - return breed, nil + return breedData, nil } diff --git a/internal/postgres/pet_store.go b/internal/postgres/pet_store.go deleted file mode 100644 index e4846334..00000000 --- a/internal/postgres/pet_store.go +++ /dev/null @@ -1,220 +0,0 @@ -package postgres - -import ( - "context" - - pnd "github.com/pet-sitter/pets-next-door-api/api" - utils "github.com/pet-sitter/pets-next-door-api/internal/common" - "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" -) - -func CreatePet(ctx context.Context, tx *database.Tx, petData *pet.Pet) (*pet.PetWithProfileImage, *pnd.AppError) { - const sql = ` - INSERT INTO - pets - ( - owner_id, - name, - pet_type, - sex, - neutered, - breed, - birth_date, - weight_in_kg, - remarks, - profile_image_id, - created_at, - updated_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW()) - RETURNING id, created_at, updated_at - ` - - if err := tx.QueryRowContext(ctx, sql, - petData.OwnerID, - petData.Name, - petData.PetType, - petData.Sex, - petData.Neutered, - petData.Breed, - petData.BirthDate, - petData.WeightInKg, - petData.Remarks, - petData.ProfileImageID, - ).Scan(&petData.ID, &petData.CreatedAt, &petData.UpdatedAt); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return FindPetByID(ctx, tx, petData.ID) -} - -func FindPetByID(ctx context.Context, tx *database.Tx, id int) (*pet.PetWithProfileImage, *pnd.AppError) { - const sql = ` - SELECT - pets.id, - pets.owner_id, - pets.name, - pets.pet_type, - pets.sex, - pets.neutered, - pets.breed, - pets.birth_date, - pets.weight_in_kg, - pets.remarks, - media.url AS profile_image_url, - pets.created_at, - pets.updated_at - FROM - pets - LEFT OUTER JOIN - media - ON - pets.profile_image_id = media.id - WHERE - pets.id = $1 AND - pets.deleted_at IS NULL - ` - - var petData pet.PetWithProfileImage - if err := tx.QueryRowContext(ctx, sql, - id, - ).Scan( - &petData.ID, - &petData.OwnerID, - &petData.Name, - &petData.PetType, - &petData.Sex, - &petData.Neutered, - &petData.Breed, - &petData.BirthDate, - &petData.WeightInKg, - &petData.Remarks, - &petData.ProfileImageURL, - &petData.CreatedAt, - &petData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - petData.BirthDate = utils.FormatDate(petData.BirthDate) - - return &petData, nil -} - -func FindPetsByOwnerID(ctx context.Context, tx *database.Tx, ownerID int) (*pet.PetWithProfileList, *pnd.AppError) { - const sql = ` - SELECT - pets.id, - pets.owner_id, - pets.name, - pets.pet_type, - pets.sex, - pets.neutered, - pets.breed, - pets.birth_date, - pets.weight_in_kg, - pets.remarks, - media.url AS profile_image_url, - pets.created_at, - pets.updated_at - FROM - pets - LEFT OUTER JOIN - media - ON - pets.profile_image_id = media.id - WHERE - pets.owner_id = $1 AND - pets.deleted_at IS NULL - ` - - var pets pet.PetWithProfileList - rows, err := tx.QueryContext(ctx, sql, - ownerID, - ) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - defer rows.Close() - - for rows.Next() { - var petData pet.PetWithProfileImage - if err := rows.Scan( - &petData.ID, - &petData.OwnerID, - &petData.Name, - &petData.PetType, - &petData.Sex, - &petData.Neutered, - &petData.Breed, - &petData.BirthDate, - &petData.WeightInKg, - &petData.Remarks, - &petData.ProfileImageURL, - &petData.CreatedAt, - &petData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - petData.BirthDate = utils.FormatDate(petData.BirthDate) - pets = append(pets, &petData) - } - if err := rows.Err(); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return &pets, nil -} - -func UpdatePet(ctx context.Context, tx *database.Tx, petID int, updatePetRequest *pet.UpdatePetRequest) *pnd.AppError { - const sql = ` - UPDATE - pets - SET - name = $1, - neutered = $2, - breed = $3, - birth_date = $4, - weight_in_kg = $5, - remarks = $6, - profile_image_id = $7, - updated_at = NOW() - WHERE - id = $8 - ` - - if _, err := tx.ExecContext(ctx, sql, - updatePetRequest.Name, - updatePetRequest.Neutered, - updatePetRequest.Breed, - updatePetRequest.BirthDate, - updatePetRequest.WeightInKg, - updatePetRequest.Remarks, - updatePetRequest.ProfileImageID, - petID, - ); err != nil { - return pnd.FromPostgresError(err) - } - - return nil -} - -func DeletePet(ctx context.Context, tx *database.Tx, petID int) *pnd.AppError { - const sql = ` - UPDATE - pets - SET - deleted_at = NOW() - WHERE - id = $1 - ` - - if _, err := tx.ExecContext(ctx, sql, - petID, - ); err != nil { - return pnd.FromPostgresError(err) - } - - return nil -} diff --git a/internal/service/breed_service.go b/internal/service/breed_service.go index 39816736..ce7bc255 100644 --- a/internal/service/breed_service.go +++ b/internal/service/breed_service.go @@ -3,8 +3,10 @@ package service import ( "context" + "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" + "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" "github.com/pet-sitter/pets-next-door-api/internal/postgres" ) @@ -21,7 +23,7 @@ func NewBreedService(conn *database.DB) *BreedService { func (s *BreedService) FindBreeds( ctx context.Context, page, size int, petType *string, -) (*pet.BreedListView, *pnd.AppError) { +) (*breed.BreedListView, *pnd.AppError) { tx, err := s.conn.BeginTx(ctx) defer tx.Rollback() if err != nil { @@ -41,15 +43,15 @@ func (s *BreedService) FindBreeds( } func (s *BreedService) FindBreedByPetTypeAndName( - ctx context.Context, petType pet.PetType, name string, -) (*pet.BreedView, *pnd.AppError) { + ctx context.Context, petType commonvo.PetType, name string, +) (*breed.BreedView, *pnd.AppError) { tx, err := s.conn.BeginTx(ctx) defer tx.Rollback() if err != nil { return nil, err } - breed, err := postgres.FindBreedByPetTypeAndName(ctx, tx, petType, name) + breedData, err := postgres.FindBreedByPetTypeAndName(ctx, tx, petType, name) if err != nil { return nil, err } @@ -58,9 +60,9 @@ func (s *BreedService) FindBreedByPetTypeAndName( return nil, err } - return &pet.BreedView{ - ID: breed.ID, - PetType: breed.PetType, - Name: breed.Name, + return &breed.BreedView{ + ID: breedData.ID, + PetType: breedData.PetType, + Name: breedData.Name, }, nil } diff --git a/internal/service/sos_post_service.go b/internal/service/sos_post_service.go index 7c4bb451..8e268aa9 100644 --- a/internal/service/sos_post_service.go +++ b/internal/service/sos_post_service.go @@ -105,7 +105,7 @@ func (service *SOSPostService) FindSOSPosts( sosPostView := sosPost.ToFindSOSPostInfoView( &user.WithoutPrivateInfo{ - ID: int(author.ID), + ID: int64(author.ID), Nickname: author.Nickname, ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), }, @@ -147,7 +147,7 @@ func (service *SOSPostService) FindSOSPostsByAuthorID( sosPostView := sosPost.ToFindSOSPostInfoView( &user.WithoutPrivateInfo{ - ID: int(author.ID), + ID: int64(author.ID), Nickname: author.Nickname, ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), }, @@ -188,7 +188,7 @@ func (service *SOSPostService) FindSOSPostByID(ctx context.Context, id int) (*so return sosPost.ToFindSOSPostInfoView( &user.WithoutPrivateInfo{ - ID: int(author.ID), + ID: int64(author.ID), Nickname: author.Nickname, ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), }, diff --git a/internal/service/tests/sos_post_service_test.go b/internal/service/tests/sos_post_service_test.go index 110212c7..c553bb31 100644 --- a/internal/service/tests/sos_post_service_test.go +++ b/internal/service/tests/sos_post_service_test.go @@ -55,8 +55,8 @@ func TestSOSPostService(t *testing.T) { // when sosPostService := service.NewSOSPostService(db) - imageIDs := []int{sosPostImage.ID, sosPostImage2.ID} - petIDs := []int{addPets.ID} + imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} + petIDs := []int64{addPets.ID} sosPostData := tests.GenerateDummyWriteSOSPostRequest(imageIDs, petIDs, 0) sosPost, err := sosPostService.WriteSOSPost(ctx, uid, sosPostData) @@ -88,10 +88,10 @@ func TestSOSPostService(t *testing.T) { if sosPost.RewardType != sosPostData.RewardType { t.Errorf("got %v want %v", sosPost.RewardType, sosPostData.RewardType) } - if sosPost.ThumbnailID != sosPostData.ImageIDs[0] { + if int64(sosPost.ThumbnailID) != sosPostData.ImageIDs[0] { t.Errorf("got %v want %v", sosPost.ThumbnailID, sosPostData.ImageIDs[0]) } - if sosPost.AuthorID != owner.ID { + if int64(sosPost.AuthorID) != owner.ID { t.Errorf("got %v want %v", sosPost.AuthorID, owner.ID) } }) @@ -120,8 +120,8 @@ func TestSOSPostService(t *testing.T) { addPets := tests.AddDummyPet(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int{sosPostImage.ID, sosPostImage2.ID} - petIDs := []int{addPets.ID} + imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} + petIDs := []int64{addPets.ID} conditionIDs := []int{1, 2} var sosPosts []sospost.WriteSOSPostView @@ -170,12 +170,12 @@ func TestSOSPostService(t *testing.T) { addPets := tests.AddDummyPets(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int{sosPostImage.ID, sosPostImage2.ID} + imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} conditionIDs := []int{1, 2} var sosPosts []sospost.WriteSOSPostView for i := 1; i < 4; i++ { - sosPost := tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int{addPets[i-1].ID}, i) + sosPost := tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int64{addPets.Pets[i-1].ID}, i) sosPosts = append(sosPosts, *sosPost) } @@ -189,7 +189,7 @@ func TestSOSPostService(t *testing.T) { for i, sosPost := range sosPostList.Items { idx := len(sosPostList.Items) - i - 1 assertConditionEquals(t, sosPost.Conditions, conditionIDs) - assertPetEquals(t, sosPost.Pets[i-1], addPets[i-1]) + assertPetEquals(t, sosPost.Pets[i-1], addPets.Pets[i-1]) assertMediaEquals(t, sosPost.Media, (&media.MediaList{sosPostImage, sosPostImage2}).ToMediaViewList()) assertAuthorEquals(t, sosPost.Author, author) assertDatesEquals(t, sosPost.Dates, sosPosts[idx].Dates) @@ -219,23 +219,27 @@ func TestSOSPostService(t *testing.T) { addPets := tests.AddDummyPets(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int{sosPostImage.ID, sosPostImage2.ID} + imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} conditionIDs := []int{1, 2} var sosPosts []sospost.WriteSOSPostView // 강아지인 경우 for i := 1; i < 3; i++ { - sosPost := tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int{addPets[i-1].ID}, i) + sosPost := tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int64{addPets.Pets[i-1].ID}, i) sosPosts = append(sosPosts, *sosPost) } // 고양이인 경우 sosPosts = append(sosPosts, - *tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int{addPets[2].ID}, 3)) + *tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int64{addPets.Pets[2].ID}, 3)) // 강아지, 고양이인 경우 sosPosts = append(sosPosts, - *tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int{addPets[1].ID, addPets[2].ID}, 4)) + *tests.WriteDummySOSPosts(t, ctx, + sosPostService, uid, imageIDs, []int64{addPets.Pets[1].ID, addPets.Pets[2].ID}, + 4, + ), + ) // when sosPostList, err := sosPostService.FindSOSPosts(ctx, 1, 3, "newest", "all") @@ -247,7 +251,7 @@ func TestSOSPostService(t *testing.T) { for i, sosPost := range sosPostList.Items { idx := len(sosPostList.Items) - i - 1 assertConditionEquals(t, sosPost.Conditions, conditionIDs) - assertPetEquals(t, sosPost.Pets[i-1], addPets[i-1]) + assertPetEquals(t, sosPost.Pets[i-1], addPets.Pets[i-1]) assertMediaEquals(t, sosPost.Media, (&media.MediaList{sosPostImage, sosPostImage2}).ToMediaViewList()) assertAuthorEquals(t, sosPost.Author, author) assertDatesEquals(t, sosPost.Dates, sosPosts[idx].Dates) @@ -276,17 +280,17 @@ func TestSOSPostService(t *testing.T) { addPet := tests.AddDummyPet(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int{sosPostImage.ID, sosPostImage2.ID} + imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} conditionIDs := []int{1, 2} sosPosts := make([]sospost.WriteSOSPostView, 0) for i := 1; i < 4; i++ { - sosPost := tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int{addPet.ID}, i) + sosPost := tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int64{addPet.ID}, i) sosPosts = append(sosPosts, *sosPost) } // when - sosPostListByAuthorID, err := sosPostService.FindSOSPostsByAuthorID(ctx, owner.ID, 1, 3, "newest", "all") + sosPostListByAuthorID, err := sosPostService.FindSOSPostsByAuthorID(ctx, int(owner.ID), 1, 3, "newest", "all") if err != nil { t.Errorf("got %v want %v", err, nil) } @@ -326,12 +330,12 @@ func TestSOSPostService(t *testing.T) { addPet := tests.AddDummyPet(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int{sosPostImage.ID, sosPostImage2.ID} + imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} conditionIDs := []int{1, 2} sosPosts := make([]sospost.WriteSOSPostView, 0) for i := 1; i < 4; i++ { - sosPost := tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int{addPet.ID}, i) + sosPost := tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, imageIDs, []int64{addPet.ID}, i) sosPosts = append(sosPosts, *sosPost) } @@ -370,7 +374,10 @@ func TestSOSPostService(t *testing.T) { addPet := tests.AddDummyPet(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - sosPost := tests.WriteDummySOSPosts(t, ctx, sosPostService, uid, []int{sosPostImage.ID}, []int{addPet.ID}, 1) + sosPost := tests.WriteDummySOSPosts(t, ctx, + sosPostService, uid, []int64{int64(sosPostImage.ID)}, []int64{addPet.ID}, + 1, + ) updateSOSPostData := &sospost.UpdateSOSPostRequest{ ID: sosPost.ID, @@ -386,7 +393,7 @@ func TestSOSPostService(t *testing.T) { CarerGender: sospost.CarerGenderMale, RewardType: sospost.RewardTypeFee, ConditionIDs: []int{1, 2}, - PetIDs: []int{addPet.ID}, + PetIDs: []int{int(addPet.ID)}, } // when @@ -421,7 +428,7 @@ func TestSOSPostService(t *testing.T) { if updateSOSPost.ThumbnailID != sosPostImage.ID { t.Errorf("got %v want %v", updateSOSPost.ThumbnailID, sosPostImage.ID) } - if updateSOSPost.AuthorID != owner.ID { + if int64(updateSOSPost.AuthorID) != owner.ID { t.Errorf("got %v want %v", updateSOSPost.AuthorID, owner.ID) } }) @@ -464,11 +471,52 @@ func assertConditionEquals(t *testing.T, got []sospost.ConditionView, want []int } } -func assertPetEquals(t *testing.T, got, want pet.PetView) { +func assertPetEquals(t *testing.T, got pet.PetView, want pet.DetailView) { t.Helper() - if !reflect.DeepEqual(got, want) { - t.Errorf("got %v want %v", got, want) + if int64(got.ID) != want.ID { + t.Errorf("got %v want %v", got.ID, want.ID) + } + + if got.Name != want.Name { + t.Errorf("got %v want %v", got.Name, want.Name) + } + + if got.PetType != want.PetType { + t.Errorf("got %v want %v", got.PetType, want.PetType) + } + + if got.Sex != want.Sex { + t.Errorf("got %v want %v", got.Sex, want.Sex) + } + + if got.Neutered != want.Neutered { + t.Errorf("got %v want %v", got.Neutered, want.Neutered) + } + + if got.Breed != want.Breed { + t.Errorf("got %v want %v", got.Breed, want.Breed) + } + + if got.BirthDate != want.BirthDate { + t.Errorf("got %v want %v", got.BirthDate, want.BirthDate) + } + + if got.WeightInKg.String() != want.WeightInKg.String() { + t.Errorf("got %v want %v", got.WeightInKg, want.WeightInKg) + } + + if got.Remarks != want.Remarks { + t.Errorf("got %v want %v", got.Remarks, want.Remarks) + } + + switch { + case got.ProfileImageURL == nil && want.ProfileImageURL != nil: + t.Errorf("got %v want %v", got.ProfileImageURL, want.ProfileImageURL) + case got.ProfileImageURL != nil && want.ProfileImageURL == nil: + t.Errorf("got %v want %v", got.ProfileImageURL, want.ProfileImageURL) + case *got.ProfileImageURL != *want.ProfileImageURL: + t.Errorf("got %v want %v", *got.ProfileImageURL, *want.ProfileImageURL) } } diff --git a/internal/service/tests/user_service_test.go b/internal/service/tests/user_service_test.go index 9a41052b..54f7e132 100644 --- a/internal/service/tests/user_service_test.go +++ b/internal/service/tests/user_service_test.go @@ -5,11 +5,15 @@ import ( "fmt" "testing" + "github.com/pet-sitter/pets-next-door-api/internal/tests/assert" + + "github.com/pet-sitter/pets-next-door-api/internal/datatype" "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/domain/user" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" "github.com/pet-sitter/pets-next-door-api/internal/service" "github.com/pet-sitter/pets-next-door-api/internal/tests" + "github.com/shopspring/decimal" ) //nolint:gocognit @@ -304,20 +308,23 @@ func TestUserService(t *testing.T) { owner := tests.RegisterDummyUser(t, ctx, userService, mediaService) profileImage := tests.AddDummyMedia(t, ctx, mediaService) - pets := pet.AddPetsToOwnerRequest{Pets: []pet.AddPetRequest{*tests.GenerateDummyAddPetRequest(&profileImage.ID)}} + petsToAdd := pet.AddPetsToOwnerRequest{ + Pets: []pet.AddPetRequest{ + *tests.GenerateDummyAddPetRequest(&profileImage.ID), + }, + } // When - userService.AddPetsToOwner(ctx, owner.FirebaseUID, pets) + created, _ := userService.AddPetsToOwner(ctx, owner.FirebaseUID, petsToAdd) // Then - found, _ := userService.FindPetsByOwnerUID(ctx, owner.FirebaseUID) - if len(found.Pets) != 1 { - t.Errorf("got %v want %v", len(found.Pets), 1) + if len(created.Pets) != 1 { + t.Errorf("got %v want %v", len(created.Pets), 1) } - for _, expected := range pets.Pets { - for _, found := range found.Pets { - assertPetRequestAndViewEquals(t, expected, found) + for _, expected := range petsToAdd.Pets { + for _, found := range created.Pets { + assert.PetRequestAndViewEquals(t, expected, found) } } }) @@ -332,35 +339,36 @@ func TestUserService(t *testing.T) { // Given mediaService := service.NewMediaService(db, nil) userService := service.NewUserService(db, mediaService) - userRequest := tests.RegisterDummyUser(t, ctx, userService, mediaService) + userData := tests.RegisterDummyUser(t, ctx, userService, mediaService) petProfileImage := tests.AddDummyMedia(t, ctx, mediaService) petRequest := tests.GenerateDummyAddPetRequest(&petProfileImage.ID) createdPets, _ := userService.AddPetsToOwner( - ctx, userRequest.FirebaseUID, pet.AddPetsToOwnerRequest{Pets: []pet.AddPetRequest{*petRequest}}) - createdPet := createdPets[0] + ctx, userData.FirebaseUID, pet.AddPetsToOwnerRequest{Pets: []pet.AddPetRequest{*petRequest}}) + createdPet := createdPets.Pets[0] // When updatedPetProfileImage := tests.AddDummyMedia(t, ctx, mediaService) + birthData, _ := datatype.ParseDate("2021-01-01") updatedPetRequest := pet.UpdatePetRequest{ Name: "updated", Neutered: true, Breed: "updated", - BirthDate: "2021-01-01", - WeightInKg: 10.0, + BirthDate: birthData, + WeightInKg: decimal.NewFromFloat(10.0), Remarks: "updated", ProfileImageID: &updatedPetProfileImage.ID, } - userService.UpdatePet(ctx, userRequest.FirebaseUID, createdPet.ID, updatedPetRequest) + userService.UpdatePet(ctx, userData.FirebaseUID, createdPet.ID, updatedPetRequest) // Then - found, _ := userService.FindPetsByOwnerUID(ctx, userRequest.FirebaseUID) + found, _ := userService.FindPets(ctx, pet.FindPetsParams{OwnerID: &userData.ID}) if len(found.Pets) != 1 { t.Errorf("got %v want %v", len(found.Pets), 1) } - assertUpdatedPetEquals(t, updatedPetRequest, found.Pets[0]) + assert.UpdatedPetEquals(t, updatedPetRequest, found.Pets[0]) }) }) @@ -373,81 +381,33 @@ func TestUserService(t *testing.T) { // Given mediaService := service.NewMediaService(db, nil) userService := service.NewUserService(db, mediaService) - userRequest := tests.RegisterDummyUser(t, ctx, userService, mediaService) + userData := tests.RegisterDummyUser(t, ctx, userService, mediaService) petProfileImage := tests.AddDummyMedia(t, ctx, mediaService) petRequest := tests.GenerateDummyAddPetRequest(&petProfileImage.ID) - createdPets, _ := userService.AddPetsToOwner( + createdPets, err := userService.AddPetsToOwner( ctx, - userRequest.FirebaseUID, + userData.FirebaseUID, pet.AddPetsToOwnerRequest{Pets: []pet.AddPetRequest{*petRequest}}, ) - createdPet := createdPets[0] + if err != nil { + t.Fatalf("got %v want %v", err, nil) + } + createdPet := createdPets.Pets[0] // When - userService.DeletePet(ctx, userRequest.FirebaseUID, createdPet.ID) + err = userService.DeletePet(ctx, userData.FirebaseUID, createdPet.ID) + if err != nil { + t.Fatalf("got %v want %v", err, nil) + } // Then - found, _ := userService.FindPetsByOwnerUID(ctx, userRequest.FirebaseUID) + found, _ := userService.FindPets(ctx, pet.FindPetsParams{ + OwnerID: &userData.ID, + }) if len(found.Pets) != 0 { - t.Errorf("got %v want %v", len(found.Pets), 0) + t.Fatalf("got %v want %v", len(found.Pets), 0) } }) }) } - -func assertPetRequestAndViewEquals(t *testing.T, expected pet.AddPetRequest, found pet.PetView) { - t.Helper() - - if expected.Name != found.Name { - t.Errorf("got %v want %v", expected.Name, found.Name) - } - - if expected.PetType != found.PetType { - t.Errorf("got %v want %v", expected.PetType, found.PetType) - } - - if expected.Sex != found.Sex { - t.Errorf("got %v want %v", expected.Sex, found.PetType) - } - - if expected.Neutered != found.Neutered { - t.Errorf("got %v want %v", expected.Neutered, found.Neutered) - } - - if expected.Breed != found.Breed { - t.Errorf("got %v want %v", expected.Breed, found.Breed) - } - - if expected.BirthDate != found.BirthDate { - t.Errorf("got %v want %v", expected.BirthDate, found.BirthDate) - } - - if expected.WeightInKg != found.WeightInKg { - t.Errorf("got %v want %v", expected.WeightInKg, found.WeightInKg) - } -} - -func assertUpdatedPetEquals(t *testing.T, expected pet.UpdatePetRequest, found pet.PetView) { - t.Helper() - - if expected.Name != found.Name { - t.Errorf("got %v want %v", expected.Name, found.Name) - } - - if expected.Neutered != found.Neutered { - t.Errorf("got %v want %v", expected.Neutered, found.Neutered) - } - - if expected.Breed != found.Breed { - t.Errorf("got %v want %v", expected.Breed, found.Breed) - } - - if expected.BirthDate != found.BirthDate { - t.Errorf("got %v want %v", expected.BirthDate, found.BirthDate) - } - - if expected.WeightInKg != found.WeightInKg { - t.Errorf("got %v want %v", expected.WeightInKg, found.WeightInKg) - } -} diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 9424c9e8..02e324c9 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" utils "github.com/pet-sitter/pets-next-door-api/internal/common" databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" @@ -140,15 +141,36 @@ func (service *UserService) DeleteUserByUID(ctx context.Context, uid string) *pn return tx.Commit() } +func (service *UserService) FindPet( + ctx context.Context, params pet.FindPetParams, +) (*pet.WithProfileImage, *pnd.AppError) { + row, err := databasegen.New(service.conn).FindPet(ctx, params.ToDBParams()) + if err != nil { + return nil, pnd.FromPostgresError(err) + } + + return pet.ToWithProfileImage(row), nil +} + +func (service *UserService) FindPets(ctx context.Context, params pet.FindPetsParams) (*pet.ListView, *pnd.AppError) { + rows, err := databasegen.New(service.conn).FindPets(ctx, params.ToDBParams()) + if err != nil { + return nil, pnd.FromPostgresError(err) + } + + return pet.ToListView(rows), nil +} + func (service *UserService) AddPetsToOwner( ctx context.Context, uid string, addPetsRequest pet.AddPetsToOwnerRequest, -) ([]pet.PetView, *pnd.AppError) { +) (*pet.ListView, *pnd.AppError) { tx, err := service.conn.BeginTx(ctx) defer tx.Rollback() if err != nil { return nil, err } + // 사용자가 존재하는지 확인 userData, err2 := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ FbUid: utils.StrToNullStr(uid), }) @@ -156,38 +178,65 @@ func (service *UserService) AddPetsToOwner( return nil, pnd.FromPostgresError(err2) } - pets := make(pet.PetWithProfileList, len(addPetsRequest.Pets)) - for i, item := range addPetsRequest.Pets { + // 프로필 이미지 ID가 DB에 존재하는지 확인 + for _, item := range addPetsRequest.Pets { if item.ProfileImageID != nil { if _, err := postgres.FindMediaByID(ctx, tx, *item.ProfileImageID); err != nil { return nil, pnd.ErrInvalidBody(fmt.Errorf("존재하지 않는 프로필 이미지 ID입니다. ID: %d", *item.ProfileImageID)) } } + } - petToCreate := item.ToPet(int(userData.ID)) - createdPet, err := postgres.CreatePet(ctx, tx, petToCreate) + // 사용자의 반려동물 추가 + petIDs := make([]int32, 0, len(addPetsRequest.Pets)) + for _, item := range addPetsRequest.Pets { + birthDate, err := time.Parse(time.DateOnly, item.BirthDate.String()) if err != nil { - return nil, err + return nil, pnd.ErrInvalidBody(fmt.Errorf("잘못된 생년월일 형식입니다. %s", item.BirthDate.String())) + } + + petToCreate := databasegen.CreatePetParams{ + OwnerID: int64(userData.ID), + Name: item.Name, + PetType: string(item.PetType), + Sex: string(item.Sex), + Neutered: item.Neutered, + Breed: item.Breed, + BirthDate: birthDate, + WeightInKg: item.WeightInKg.String(), + Remarks: item.Remarks, + ProfileImageID: utils.IntPtrToNullInt64(item.ProfileImageID), + } + row, err := databasegen.New(service.conn).WithTx(tx.Tx).CreatePet(ctx, petToCreate) + if err != nil { + return nil, pnd.FromPostgresError(err) } - pets[i] = createdPet + petIDs = append(petIDs, row.ID) } if err := tx.Commit(); err != nil { return nil, err } - return pets.ToPetViewList(), nil + rows, err2 := databasegen.New(service.conn).FindPetsByIDs(ctx, databasegen.FindPetsByIDsParams{ + Ids: petIDs, + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) + } + + return pet.ToListViewFromIDsRows(rows), nil } func (service *UserService) UpdatePet( - ctx context.Context, uid string, petID int, updatePetRequest pet.UpdatePetRequest, -) (*pet.PetView, *pnd.AppError) { + ctx context.Context, uid string, petID int64, updatePetRequest pet.UpdatePetRequest, +) (*pet.DetailView, *pnd.AppError) { owner, err := service.FindUser(ctx, user.FindUserParams{FbUID: &uid, IncludeDeleted: false}) if err != nil { return nil, err } - petToUpdate, err := service.findPetByID(ctx, petID) + petToUpdate, err := service.FindPet(ctx, pet.FindPetParams{ID: &petID, IncludeDeleted: false}) if err != nil { return nil, err } @@ -208,28 +257,42 @@ func (service *UserService) UpdatePet( return nil, err } - err = postgres.UpdatePet(ctx, tx, petID, &updatePetRequest) - if err != nil { - return nil, err + birthDate, err2 := time.Parse(time.DateOnly, updatePetRequest.BirthDate.String()) + if err2 != nil { + return nil, pnd.ErrInvalidBody(fmt.Errorf("잘못된 생년월일 형식입니다. %s", updatePetRequest.BirthDate.String())) + } + + if err := databasegen.New(service.conn).WithTx(tx.Tx).UpdatePet(ctx, databasegen.UpdatePetParams{ + ID: int32(petID), + Name: updatePetRequest.Name, + Neutered: updatePetRequest.Neutered, + Breed: updatePetRequest.Breed, + BirthDate: birthDate, + WeightInKg: updatePetRequest.WeightInKg.String(), + Remarks: updatePetRequest.Remarks, + ProfileImageID: utils.IntPtrToNullInt64(updatePetRequest.ProfileImageID), + }); err != nil { + return nil, pnd.FromPostgresError(err) } + if err = tx.Commit(); err != nil { return nil, err } - updatedPet, err := service.findPetByID(ctx, petID) + updatedPet, err := service.FindPet(ctx, pet.FindPetParams{ID: &petID, IncludeDeleted: false}) if err != nil { return nil, err } - return updatedPet.ToPetView(), nil + return updatedPet.ToDetailView(), nil } -func (service *UserService) DeletePet(ctx context.Context, uid string, petID int) *pnd.AppError { - owner, err := service.FindUser(ctx, user.FindUserParams{FbUID: &uid, IncludeDeleted: false}) +func (service *UserService) DeletePet(ctx context.Context, uid string, petID int64) *pnd.AppError { + owner, err := service.FindUser(ctx, user.FindUserParams{FbUID: &uid}) if err != nil { return err } - petToDelete, err := service.findPetByID(ctx, petID) + petToDelete, err := service.FindPet(ctx, pet.FindPetParams{ID: &petID}) if err != nil { return err } @@ -244,55 +307,9 @@ func (service *UserService) DeletePet(ctx context.Context, uid string, petID int return err } - if err := postgres.DeletePet(ctx, tx, petID); err != nil { - return err + if err := databasegen.New(service.conn).WithTx(tx.Tx).DeletePet(ctx, int32(petID)); err != nil { + return pnd.FromPostgresError(err) } return tx.Commit() } - -func (service *UserService) FindPetsByOwnerUID(ctx context.Context, uid string) (*pet.FindMyPetsView, *pnd.AppError) { - userData, err2 := databasegen.New(service.conn).FindUser(ctx, databasegen.FindUserParams{ - FbUid: utils.StrToNullStr(uid), - IncludeDeleted: false, - }) - if err2 != nil { - return nil, pnd.FromPostgresError(err2) - } - - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return nil, err - } - - pets, err := postgres.FindPetsByOwnerID(ctx, tx, int(userData.ID)) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return pets.ToFindMyPetsView(), nil -} - -func (service *UserService) findPetByID(ctx context.Context, petID int) (*pet.PetWithProfileImage, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return nil, err - } - - petData, err := postgres.FindPetByID(ctx, tx, petID) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return petData, nil -} diff --git a/internal/tests/assert/pet.go b/internal/tests/assert/pet.go new file mode 100644 index 00000000..c9652cca --- /dev/null +++ b/internal/tests/assert/pet.go @@ -0,0 +1,63 @@ +package assert + +import ( + "testing" + + "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" +) + +func PetRequestAndViewEquals(t *testing.T, expected pet.AddPetRequest, found pet.DetailView) { + t.Helper() + + if expected.Name != found.Name { + t.Errorf("got %v want %v", expected.Name, found.Name) + } + + if expected.PetType != found.PetType { + t.Errorf("got %v want %v", expected.PetType, found.PetType) + } + + if expected.Sex != found.Sex { + t.Errorf("got %v want %v", expected.Sex, found.PetType) + } + + if expected.Neutered != found.Neutered { + t.Errorf("got %v want %v", expected.Neutered, found.Neutered) + } + + if expected.Breed != found.Breed { + t.Errorf("got %v want %v", expected.Breed, found.Breed) + } + + if expected.BirthDate != found.BirthDate { + t.Errorf("got %v want %v", expected.BirthDate, found.BirthDate) + } + + if expected.WeightInKg.String() != found.WeightInKg.String() { + t.Errorf("got %v want %v", expected.WeightInKg, found.WeightInKg) + } +} + +func UpdatedPetEquals(t *testing.T, expected pet.UpdatePetRequest, found pet.DetailView) { + t.Helper() + + if expected.Name != found.Name { + t.Errorf("got %v want %v", expected.Name, found.Name) + } + + if expected.Neutered != found.Neutered { + t.Errorf("got %v want %v", expected.Neutered, found.Neutered) + } + + if expected.Breed != found.Breed { + t.Errorf("got %v want %v", expected.Breed, found.Breed) + } + + if expected.BirthDate != found.BirthDate { + t.Errorf("got %v want %v", expected.BirthDate, found.BirthDate) + } + + if expected.WeightInKg.String() != found.WeightInKg.String() { + t.Errorf("got %v want %v", expected.WeightInKg, found.WeightInKg) + } +} diff --git a/internal/tests/factories.go b/internal/tests/factories.go index 89bb9cfa..e8cbda45 100644 --- a/internal/tests/factories.go +++ b/internal/tests/factories.go @@ -3,9 +3,11 @@ package tests import ( "fmt" + "github.com/pet-sitter/pets-next-door-api/internal/datatype" "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/domain/sospost" "github.com/pet-sitter/pets-next-door-api/internal/domain/user" + "github.com/shopspring/decimal" ) func GenerateDummyRegisterUserRequest(profileImageID *int) *user.RegisterUserRequest { @@ -20,19 +22,24 @@ func GenerateDummyRegisterUserRequest(profileImageID *int) *user.RegisterUserReq } func GenerateDummyAddPetRequest(profileImageID *int) *pet.AddPetRequest { + birthDate, _ := datatype.ParseDate("2020-01-01") return &pet.AddPetRequest{ Name: "name", PetType: "dog", Sex: "male", Neutered: true, Breed: "poodle", - BirthDate: "2020-01-01", - WeightInKg: 10.0, + BirthDate: birthDate, + WeightInKg: decimal.NewFromFloat(10.0), ProfileImageID: profileImageID, } } func GenerateDummyAddPetsRequest(profileImageID *int) []pet.AddPetRequest { + birthDate1, _ := datatype.ParseDate("2020-01-01") + birthDate2, _ := datatype.ParseDate("2020-02-01") + birthDate3, _ := datatype.ParseDate("2020-03-01") + return []pet.AddPetRequest{ { Name: "dog_1", @@ -40,8 +47,8 @@ func GenerateDummyAddPetsRequest(profileImageID *int) []pet.AddPetRequest { Sex: "male", Neutered: true, Breed: "poodle", - BirthDate: "2020-01-01", - WeightInKg: 10.0, + BirthDate: birthDate1, + WeightInKg: decimal.NewFromFloat(10.0), Remarks: "remarks", ProfileImageID: profileImageID, }, @@ -51,8 +58,8 @@ func GenerateDummyAddPetsRequest(profileImageID *int) []pet.AddPetRequest { Sex: "male", Neutered: true, Breed: "poodle", - BirthDate: "2020-02-01", - WeightInKg: 10.0, + BirthDate: birthDate2, + WeightInKg: decimal.NewFromFloat(10.0), Remarks: "remarks", ProfileImageID: profileImageID, }, @@ -62,15 +69,15 @@ func GenerateDummyAddPetsRequest(profileImageID *int) []pet.AddPetRequest { Sex: "female", Neutered: true, Breed: "munchkin", - BirthDate: "2020-03-01", - WeightInKg: 8.0, + BirthDate: birthDate3, + WeightInKg: decimal.NewFromFloat(8.0), Remarks: "remarks", ProfileImageID: profileImageID, }, } } -func GenerateDummyWriteSOSPostRequest(imageID, petIDs []int, sosPostCnt int) *sospost.WriteSOSPostRequest { +func GenerateDummyWriteSOSPostRequest(imageID, petIDs []int64, sosPostCnt int) *sospost.WriteSOSPostRequest { return &sospost.WriteSOSPostRequest{ Title: fmt.Sprintf("Title%d", sosPostCnt), Content: fmt.Sprintf("Content%d", sosPostCnt), diff --git a/internal/tests/service.go b/internal/tests/service.go index 7af4b996..309f5093 100644 --- a/internal/tests/service.go +++ b/internal/tests/service.go @@ -47,16 +47,16 @@ func AddDummyPet( userService *service.UserService, ownerUID string, profileImageID *int, -) *pet.PetView { +) *pet.DetailView { t.Helper() - pets, err := userService.AddPetsToOwner(ctx, ownerUID, pet.AddPetsToOwnerRequest{ + petList, err := userService.AddPetsToOwner(ctx, ownerUID, pet.AddPetsToOwnerRequest{ Pets: []pet.AddPetRequest{*GenerateDummyAddPetRequest(profileImageID)}, }) if err != nil { t.Errorf("got %v want %v", err, nil) } - return &pets[0] + return &petList.Pets[0] } func AddDummyPets( @@ -65,16 +65,16 @@ func AddDummyPets( userService *service.UserService, ownerUID string, profileImageID *int, -) []pet.PetView { +) pet.ListView { t.Helper() - pets, err := userService.AddPetsToOwner(ctx, ownerUID, pet.AddPetsToOwnerRequest{ + petList, err := userService.AddPetsToOwner(ctx, ownerUID, pet.AddPetsToOwnerRequest{ Pets: GenerateDummyAddPetsRequest(profileImageID), }) if err != nil { t.Errorf("got %v want %v", err, nil) } - return pets + return *petList } func WriteDummySOSPosts( @@ -82,8 +82,8 @@ func WriteDummySOSPosts( ctx context.Context, sosPostService *service.SOSPostService, uid string, - imageID []int, - petIDs []int, + imageID []int64, + petIDs []int64, sosPostCnt int, ) *sospost.WriteSOSPostView { t.Helper() diff --git a/queries/pets.sql b/queries/pets.sql new file mode 100644 index 00000000..e11f185f --- /dev/null +++ b/queries/pets.sql @@ -0,0 +1,113 @@ +-- name: CreatePet :one +INSERT INTO pets +(owner_id, + name, + pet_type, + sex, + neutered, + breed, + birth_date, + weight_in_kg, + remarks, + profile_image_id, + created_at, + updated_at) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW()) +RETURNING id, created_at, updated_at; + +-- name: FindPet :one +SELECT pets.id, + pets.owner_id, + pets.name, + pets.pet_type, + pets.sex, + pets.neutered, + pets.breed, + pets.birth_date, + pets.weight_in_kg, + pets.remarks, + media.url AS profile_image_url, + pets.created_at, + pets.updated_at, + pets.deleted_at +FROM pets + LEFT OUTER JOIN + media + ON + pets.profile_image_id = media.id +WHERE (pets.id = sqlc.narg('id') OR sqlc.narg('id') IS NULL) + AND (pets.owner_id = sqlc.narg('owner_id') OR sqlc.narg('owner_id') IS NULL) + AND (sqlc.arg('include_deleted')::boolean = TRUE OR + (sqlc.arg('include_deleted')::boolean = FALSE AND pets.deleted_at IS NULL)) +LIMIT 1; + +-- name: FindPets :many +SELECT pets.id, + pets.owner_id, + pets.name, + pets.pet_type, + pets.sex, + pets.neutered, + pets.breed, + pets.birth_date, + pets.weight_in_kg, + pets.remarks, + media.url AS profile_image_url, + pets.created_at, + pets.updated_at, + pets.deleted_at +FROM pets + LEFT OUTER JOIN + media + ON + pets.profile_image_id = media.id +WHERE (pets.id = sqlc.narg('id') OR sqlc.narg('id') IS NULL) + AND (pets.owner_id = sqlc.narg('owner_id') OR sqlc.narg('owner_id') IS NULL) + AND (sqlc.arg('include_deleted')::boolean = TRUE OR + (sqlc.arg('include_deleted')::boolean = FALSE AND pets.deleted_at IS NULL)) +ORDER BY pets.created_at DESC +LIMIT $1 OFFSET $2; + +-- name: FindPetsByIDs :many +SELECT pets.id, + pets.owner_id, + pets.name, + pets.pet_type, + pets.sex, + pets.neutered, + pets.breed, + pets.birth_date, + pets.weight_in_kg, + pets.remarks, + media.url AS profile_image_url, + pets.created_at, + pets.updated_at, + pets.deleted_at +FROM pets + LEFT OUTER JOIN + media + ON + pets.profile_image_id = media.id +WHERE pets.id = ANY (sqlc.arg('ids')::int[]) + AND (sqlc.arg('include_deleted')::boolean = TRUE OR + (sqlc.arg('include_deleted')::boolean = FALSE AND pets.deleted_at IS NULL)) +ORDER BY pets.created_at DESC; + +-- name: UpdatePet :exec +UPDATE + pets +SET name = $2, + neutered = $3, + breed = $4, + birth_date = $5, + weight_in_kg = $6, + remarks = $7, + profile_image_id = $8, + updated_at = NOW() +WHERE id = $1; + +-- name: DeletePet :exec +UPDATE + pets +SET deleted_at = NOW() +WHERE id = $1; From 44d10f9a9fb3c12197bd95d487e4b21abca68401 Mon Sep 17 00:00:00 2001 From: litsynp Date: Thu, 25 Apr 2024 03:23:05 +0900 Subject: [PATCH 07/12] refactor: reduce views --- internal/domain/pet/model.go | 44 +++----------- internal/domain/pet/request.go | 23 +------ internal/domain/pet/view.go | 51 +--------------- internal/domain/pet/vmodel.go | 60 +++++++++++++++++++ internal/domain/sospost/sos_post.go | 2 +- internal/domain/sospost/view.go | 14 ++--- internal/postgres/sos_post_store.go | 6 +- internal/service/sos_post_service.go | 10 ++-- .../service/tests/sos_post_service_test.go | 4 +- 9 files changed, 89 insertions(+), 125 deletions(-) create mode 100644 internal/domain/pet/vmodel.go diff --git a/internal/domain/pet/model.go b/internal/domain/pet/model.go index d97dc5b7..1d3317ad 100644 --- a/internal/domain/pet/model.go +++ b/internal/domain/pet/model.go @@ -11,41 +11,11 @@ import ( "github.com/shopspring/decimal" ) -type BasePet struct { - ID int `field:"id" json:"id"` - OwnerID int `field:"owner_id" json:"owner_id"` - Name string `field:"name" json:"name"` - PetType commonvo.PetType `field:"pet_type" json:"pet_type"` - Sex PetSex `field:"sex" json:"sex"` - Neutered bool `field:"neutered" json:"neutered"` - Breed string `field:"breed" json:"breed"` - BirthDate datatype.Date `field:"birth_date" json:"birth_date"` - WeightInKg decimal.Decimal `field:"weight_in_kg" json:"weight_in_kg"` - Remarks string `field:"remarks" json:"remarks"` - CreatedAt string `field:"created_at" json:"created_at"` - UpdatedAt string `field:"updated_at" json:"updated_at"` - DeletedAt string `field:"deleted_at" json:"deleted_at"` -} - -type Pet struct { - BasePet - ProfileImageID *int `field:"profile_image_id"` -} - -type PetList []*Pet - -type PetWithProfileImage struct { - BasePet - ProfileImageURL *string `field:"profile_image_url" json:"profile_image_url"` -} - -type PetWithProfileList []*PetWithProfileImage - -type PetSex string +type Gender string const ( - PetSexMale PetSex = "male" - PetSexFemale PetSex = "female" + GenderMale Gender = "male" + GenderFemale Gender = "female" ) type WithProfileImage struct { @@ -53,7 +23,7 @@ type WithProfileImage struct { OwnerID int64 Name string PetType commonvo.PetType - Sex PetSex + Sex Gender Neutered bool Breed string BirthDate datatype.Date @@ -74,7 +44,7 @@ func ToWithProfileImage(row databasegen.FindPetRow) *WithProfileImage { OwnerID: row.OwnerID, Name: row.Name, PetType: commonvo.PetType(row.PetType), - Sex: PetSex(row.Sex), + Sex: Gender(row.Sex), Neutered: row.Neutered, Breed: row.Breed, BirthDate: birthDate, @@ -96,7 +66,7 @@ func ToWithProfileImageFromRows(row databasegen.FindPetsRow) *WithProfileImage { OwnerID: row.OwnerID, Name: row.Name, PetType: commonvo.PetType(row.PetType), - Sex: PetSex(row.Sex), + Sex: Gender(row.Sex), Neutered: row.Neutered, Breed: row.Breed, BirthDate: birthDate, @@ -118,7 +88,7 @@ func ToWithProfileImageFromIDsRows(row databasegen.FindPetsByIDsRow) *WithProfil OwnerID: row.OwnerID, Name: row.Name, PetType: commonvo.PetType(row.PetType), - Sex: PetSex(row.Sex), + Sex: Gender(row.Sex), Neutered: row.Neutered, Breed: row.Breed, BirthDate: birthDate, diff --git a/internal/domain/pet/request.go b/internal/domain/pet/request.go index 2b022e40..3438c486 100644 --- a/internal/domain/pet/request.go +++ b/internal/domain/pet/request.go @@ -13,7 +13,7 @@ type AddPetsToOwnerRequest struct { type AddPetRequest struct { Name string `json:"name" validate:"required"` PetType commonvo.PetType `json:"petType" validate:"required,oneof=dog cat"` - Sex PetSex `json:"sex" validate:"required,oneof=male female"` + Sex Gender `json:"sex" validate:"required,oneof=male female"` Neutered bool `json:"neutered" validate:"required"` Breed string `json:"breed" validate:"required"` BirthDate utils.Date `json:"birthDate" validate:"required"` @@ -22,27 +22,6 @@ type AddPetRequest struct { ProfileImageID *int `json:"profileImageId"` } -func (r *AddPetRequest) ToBasePet(ownerID int) *BasePet { - return &BasePet{ - OwnerID: ownerID, - Name: r.Name, - PetType: r.PetType, - Sex: r.Sex, - Neutered: r.Neutered, - Breed: r.Breed, - BirthDate: r.BirthDate, - WeightInKg: r.WeightInKg, - Remarks: r.Remarks, - } -} - -func (r *AddPetRequest) ToPet(ownerID int) *Pet { - return &Pet{ - BasePet: *r.ToBasePet(ownerID), - ProfileImageID: r.ProfileImageID, - } -} - type UpdatePetRequest struct { Name string `json:"name" validate:"required"` Neutered bool `json:"neutered" validate:"required"` diff --git a/internal/domain/pet/view.go b/internal/domain/pet/view.go index 5af31b04..022696e2 100644 --- a/internal/domain/pet/view.go +++ b/internal/domain/pet/view.go @@ -7,11 +7,11 @@ import ( "github.com/shopspring/decimal" ) -type PetView struct { +type LegacyView struct { ID int `json:"id"` Name string `json:"name"` PetType commonvo.PetType `json:"petType"` - Sex PetSex `json:"sex"` + Sex Gender `json:"sex"` Neutered bool `json:"neutered"` Breed string `json:"breed"` BirthDate utils.Date `json:"birthDate"` @@ -20,48 +20,11 @@ type PetView struct { ProfileImageURL *string `json:"profileImageUrl"` } -func (pet *Pet) ToPetView() *PetView { - return &PetView{ - ID: pet.ID, - Name: pet.Name, - PetType: pet.PetType, - Sex: pet.Sex, - Neutered: pet.Neutered, - Breed: pet.Breed, - BirthDate: pet.BirthDate, - WeightInKg: pet.WeightInKg, - Remarks: pet.Remarks, - } -} - -func (pets *PetList) ToPetViewList() []PetView { - petViews := make([]PetView, len(*pets)) - for i, pet := range *pets { - petViews[i] = *pet.ToPetView() - } - return petViews -} - -func (pet *PetWithProfileImage) ToPetView() *PetView { - return &PetView{ - ID: pet.ID, - Name: pet.Name, - PetType: pet.PetType, - Sex: pet.Sex, - Neutered: pet.Neutered, - Breed: pet.Breed, - BirthDate: pet.BirthDate, - WeightInKg: pet.WeightInKg, - Remarks: pet.Remarks, - ProfileImageURL: pet.ProfileImageURL, - } -} - type DetailView struct { ID int64 `json:"id"` Name string `json:"name"` PetType commonvo.PetType `json:"petType"` - Sex PetSex `json:"sex"` + Sex Gender `json:"sex"` Neutered bool `json:"neutered"` Breed string `json:"breed"` BirthDate utils.Date `json:"birthDate"` @@ -104,11 +67,3 @@ func ToListViewFromIDsRows(rows []databasegen.FindPetsByIDsRow) *ListView { } return pl } - -func (pets *PetWithProfileList) ToPetViewList() []PetView { - petViews := make([]PetView, len(*pets)) - for i, pet := range *pets { - petViews[i] = *pet.ToPetView() - } - return petViews -} diff --git a/internal/domain/pet/vmodel.go b/internal/domain/pet/vmodel.go new file mode 100644 index 00000000..8d453c27 --- /dev/null +++ b/internal/domain/pet/vmodel.go @@ -0,0 +1,60 @@ +package pet + +import ( + "github.com/pet-sitter/pets-next-door-api/internal/datatype" + "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + "github.com/shopspring/decimal" +) + +type BasePet struct { + ID int `field:"id" json:"id"` + OwnerID int `field:"owner_id" json:"owner_id"` + Name string `field:"name" json:"name"` + PetType commonvo.PetType `field:"pet_type" json:"pet_type"` + Sex Gender `field:"sex" json:"sex"` + Neutered bool `field:"neutered" json:"neutered"` + Breed string `field:"breed" json:"breed"` + BirthDate datatype.Date `field:"birth_date" json:"birth_date"` + WeightInKg decimal.Decimal `field:"weight_in_kg" json:"weight_in_kg"` + Remarks string `field:"remarks" json:"remarks"` + CreatedAt string `field:"created_at" json:"created_at"` + UpdatedAt string `field:"updated_at" json:"updated_at"` + DeletedAt string `field:"deleted_at" json:"deleted_at"` +} + +type Pet struct { + BasePet + ProfileImageID *int `field:"profile_image_id"` +} + +type PetList []*Pet + +type ViewForSOSPost struct { + BasePet + ProfileImageURL *string `field:"profile_image_url" json:"profile_image_url"` +} + +func (v *ViewForSOSPost) ToDetailView() *DetailView { + return &DetailView{ + ID: int64(v.ID), + Name: v.Name, + PetType: v.PetType, + Sex: v.Sex, + Neutered: v.Neutered, + Breed: v.Breed, + BirthDate: v.BirthDate, + WeightInKg: v.WeightInKg, + Remarks: v.Remarks, + ProfileImageURL: v.ProfileImageURL, + } +} + +type ViewListForSOSPost []*ViewForSOSPost + +func (vl *ViewListForSOSPost) ToDetailViewList() []DetailView { + pl := make([]DetailView, len(*vl)) + for i, v := range *vl { + pl[i] = *v.ToDetailView() + } + return pl +} diff --git a/internal/domain/sospost/sos_post.go b/internal/domain/sospost/sos_post.go index 05e40e69..58d717bc 100644 --- a/internal/domain/sospost/sos_post.go +++ b/internal/domain/sospost/sos_post.go @@ -59,7 +59,7 @@ type SOSPostInfo struct { Content string `field:"content" json:"content"` Media media.MediaList `field:"media" json:"media"` Conditions ConditionList `field:"conditions" json:"conditions"` - Pets pet.PetWithProfileList `field:"pets" json:"pets"` + Pets pet.ViewListForSOSPost `field:"pets" json:"pets"` Reward string `field:"reward" json:"reward"` Dates SOSDatesList `field:"dates" json:"dates"` CareType CareType `field:"careType" json:"careType"` diff --git a/internal/domain/sospost/view.go b/internal/domain/sospost/view.go index b960517d..0a11cb0e 100644 --- a/internal/domain/sospost/view.go +++ b/internal/domain/sospost/view.go @@ -50,7 +50,7 @@ type WriteSOSPostView struct { Content string `json:"content"` Media media.MediaViewList `json:"media"` Conditions []ConditionView `json:"conditions"` - Pets []pet.PetView `json:"pets"` + Pets []pet.DetailView `json:"pets"` Reward string `json:"reward"` Dates []SOSDateView `json:"dates"` CareType CareType `json:"careType"` @@ -64,7 +64,7 @@ type WriteSOSPostView struct { func (p *SOSPost) ToWriteSOSPostView( mediaList media.MediaViewList, conditions []ConditionView, - pets []pet.PetView, + pets []pet.DetailView, sosDates []SOSDateView, ) *WriteSOSPostView { return &WriteSOSPostView{ @@ -93,7 +93,7 @@ type FindSOSPostView struct { Content string `json:"content"` Media media.MediaViewList `json:"media"` Conditions []ConditionView `json:"conditions"` - Pets []pet.PetView `json:"pets"` + Pets []pet.DetailView `json:"pets"` Reward string `json:"reward"` Dates []SOSDateView `json:"dates"` CareType CareType `json:"careType"` @@ -108,7 +108,7 @@ func (p *SOSPost) ToFindSOSPostView( author *user.WithoutPrivateInfo, mediaList media.MediaViewList, conditions []ConditionView, - pets []pet.PetView, + pets []pet.DetailView, sosDates []SOSDateView, ) *FindSOSPostView { return &FindSOSPostView{ @@ -154,7 +154,7 @@ func (p *SOSPostInfo) ToFindSOSPostInfoView( author *user.WithoutPrivateInfo, mediaList media.MediaViewList, conditions []ConditionView, - pets []pet.PetView, + pets []pet.DetailView, sosDates []SOSDateView, ) *FindSOSPostView { return &FindSOSPostView{ @@ -197,7 +197,7 @@ type UpdateSOSPostView struct { Content string `json:"content"` Media media.MediaViewList `json:"media"` Conditions []ConditionView `json:"conditions"` - Pets []pet.PetView `json:"pets"` + Pets []pet.DetailView `json:"pets"` Reward string `json:"reward"` Dates []SOSDateView `json:"dates"` CareType CareType `json:"careType"` @@ -211,7 +211,7 @@ type UpdateSOSPostView struct { func (p *SOSPost) ToUpdateSOSPostView( mediaList media.MediaViewList, conditions []ConditionView, - pets []pet.PetView, + pets []pet.DetailView, sosDates []SOSDateView, ) *UpdateSOSPostView { return &UpdateSOSPostView{ diff --git a/internal/postgres/sos_post_store.go b/internal/postgres/sos_post_store.go index 377855cb..5ba9b680 100644 --- a/internal/postgres/sos_post_store.go +++ b/internal/postgres/sos_post_store.go @@ -742,7 +742,7 @@ func FindConditionByID(ctx context.Context, tx *database.Tx, id int) (*sospost.C return &conditions, nil } -func FindPetsByID(ctx context.Context, tx *database.Tx, id int) (*pet.PetWithProfileList, *pnd.AppError) { +func FindPetsByID(ctx context.Context, tx *database.Tx, id int) (*pet.ViewListForSOSPost, *pnd.AppError) { const query = ` SELECT pets.id, @@ -774,7 +774,7 @@ func FindPetsByID(ctx context.Context, tx *database.Tx, id int) (*pet.PetWithPro ` - pets := pet.PetWithProfileList{} + pets := pet.ViewListForSOSPost{} rows, err := tx.QueryContext(ctx, query, id) if err != nil { return nil, pnd.FromPostgresError(err) @@ -782,7 +782,7 @@ func FindPetsByID(ctx context.Context, tx *database.Tx, id int) (*pet.PetWithPro defer rows.Close() for rows.Next() { - petData := pet.PetWithProfileImage{} + petData := pet.ViewForSOSPost{} if err := rows.Scan( &petData.ID, &petData.OwnerID, diff --git a/internal/service/sos_post_service.go b/internal/service/sos_post_service.go index 8e268aa9..1e7f8d1c 100644 --- a/internal/service/sos_post_service.go +++ b/internal/service/sos_post_service.go @@ -73,7 +73,7 @@ func (service *SOSPostService) WriteSOSPost( return sosPost.ToWriteSOSPostView( mediaData.ToMediaViewList(), conditions.ToConditionViewList(), - pets.ToPetViewList(), + pets.ToDetailViewList(), dates.ToSOSDateViewList(), ), nil } @@ -111,7 +111,7 @@ func (service *SOSPostService) FindSOSPosts( }, sosPost.Media.ToMediaViewList(), sosPost.Conditions.ToConditionViewList(), - sosPost.Pets.ToPetViewList(), + sosPost.Pets.ToDetailViewList(), sosPost.Dates.ToSOSDateViewList(), ) @@ -153,7 +153,7 @@ func (service *SOSPostService) FindSOSPostsByAuthorID( }, sosPost.Media.ToMediaViewList(), sosPost.Conditions.ToConditionViewList(), - sosPost.Pets.ToPetViewList(), + sosPost.Pets.ToDetailViewList(), sosPost.Dates.ToSOSDateViewList(), ) @@ -194,7 +194,7 @@ func (service *SOSPostService) FindSOSPostByID(ctx context.Context, id int) (*so }, sosPost.Media.ToMediaViewList(), sosPost.Conditions.ToConditionViewList(), - sosPost.Pets.ToPetViewList(), + sosPost.Pets.ToDetailViewList(), sosPost.Dates.ToSOSDateViewList(), ), nil } @@ -240,7 +240,7 @@ func (service *SOSPostService) UpdateSOSPost( return updateSOSPost.ToUpdateSOSPostView( mediaData.ToMediaViewList(), conditions.ToConditionViewList(), - pets.ToPetViewList(), + pets.ToDetailViewList(), dates.ToSOSDateViewList(), ), nil } diff --git a/internal/service/tests/sos_post_service_test.go b/internal/service/tests/sos_post_service_test.go index c553bb31..6978a69d 100644 --- a/internal/service/tests/sos_post_service_test.go +++ b/internal/service/tests/sos_post_service_test.go @@ -471,10 +471,10 @@ func assertConditionEquals(t *testing.T, got []sospost.ConditionView, want []int } } -func assertPetEquals(t *testing.T, got pet.PetView, want pet.DetailView) { +func assertPetEquals(t *testing.T, got, want pet.DetailView) { t.Helper() - if int64(got.ID) != want.ID { + if got.ID != want.ID { t.Errorf("got %v want %v", got.ID, want.ID) } From 6421b23955cb7ae428e8fe72576cfa33c49488eb Mon Sep 17 00:00:00 2001 From: litsynp Date: Thu, 25 Apr 2024 23:46:04 +0900 Subject: [PATCH 08/12] refactor: replace FindPetsByID in sospost with sqlc --- internal/common/null.go | 7 +++ internal/domain/pet/model.go | 66 ++++++++++++++++++++ internal/domain/pet/view.go | 8 +++ internal/domain/pet/vmodel.go | 60 ------------------ internal/domain/sospost/sos_post.go | 2 +- internal/infra/database/gen/pets.sql.go | 83 +++++++++++++++++++++++++ internal/postgres/sos_post_store.go | 68 -------------------- internal/service/sos_post_service.go | 18 +++--- queries/pets.sql | 27 ++++++++ 9 files changed, 202 insertions(+), 137 deletions(-) delete mode 100644 internal/domain/pet/vmodel.go diff --git a/internal/common/null.go b/internal/common/null.go index f588e449..212f0fc6 100644 --- a/internal/common/null.go +++ b/internal/common/null.go @@ -42,6 +42,13 @@ func NullInt64ToInt64Ptr(val sql.NullInt64) *int64 { return nil } +func IntToNullInt64(val int) sql.NullInt64 { + return sql.NullInt64{ + Int64: int64(val), + Valid: val != 0, + } +} + func Int64PtrToNullInt64(val *int64) sql.NullInt64 { return sql.NullInt64{ Int64: DerefOrEmpty(val), diff --git a/internal/domain/pet/model.go b/internal/domain/pet/model.go index 1d3317ad..499bfe20 100644 --- a/internal/domain/pet/model.go +++ b/internal/domain/pet/model.go @@ -100,3 +100,69 @@ func ToWithProfileImageFromIDsRows(row databasegen.FindPetsByIDsRow) *WithProfil DeletedAt: row.DeletedAt, } } + +func ToWithProfileImageFromSOSPostIDRow(row databasegen.FindPetsBySOSPostIDRow) *WithProfileImage { + weightInKg, _ := decimal.NewFromString(row.WeightInKg) + birthDate := datatype.DateOf(row.BirthDate) + + return &WithProfileImage{ + ID: int64(row.ID), + OwnerID: row.OwnerID, + Name: row.Name, + PetType: commonvo.PetType(row.PetType), + Sex: Gender(row.Sex), + Neutered: row.Neutered, + Breed: row.Breed, + BirthDate: birthDate, + WeightInKg: weightInKg, + Remarks: row.Remarks, + ProfileImageURL: utils.NullStrToStrPtr(row.ProfileImageUrl), + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, + DeletedAt: row.DeletedAt, + } +} + +// ViewForSOSPost +// v_pets_for_sos_posts 뷰를 위한 구조체 +type ViewForSOSPost struct { + ID int `field:"id" json:"id"` + OwnerID int `field:"owner_id" json:"owner_id"` + Name string `field:"name" json:"name"` + PetType commonvo.PetType `field:"pet_type" json:"pet_type"` + Sex Gender `field:"sex" json:"sex"` + Neutered bool `field:"neutered" json:"neutered"` + Breed string `field:"breed" json:"breed"` + BirthDate datatype.Date `field:"birth_date" json:"birth_date"` + WeightInKg decimal.Decimal `field:"weight_in_kg" json:"weight_in_kg"` + Remarks string `field:"remarks" json:"remarks"` + CreatedAt string `field:"created_at" json:"created_at"` + UpdatedAt string `field:"updated_at" json:"updated_at"` + DeletedAt string `field:"deleted_at" json:"deleted_at"` + ProfileImageURL *string `field:"profile_image_url" json:"profile_image_url"` +} + +func (v *ViewForSOSPost) ToDetailView() *DetailView { + return &DetailView{ + ID: int64(v.ID), + Name: v.Name, + PetType: v.PetType, + Sex: v.Sex, + Neutered: v.Neutered, + Breed: v.Breed, + BirthDate: v.BirthDate, + WeightInKg: v.WeightInKg, + Remarks: v.Remarks, + ProfileImageURL: v.ProfileImageURL, + } +} + +type ViewListForSOSPost []*ViewForSOSPost + +func (vl *ViewListForSOSPost) ToDetailViewList() []DetailView { + pl := make([]DetailView, len(*vl)) + for i, v := range *vl { + pl[i] = *v.ToDetailView() + } + return pl +} diff --git a/internal/domain/pet/view.go b/internal/domain/pet/view.go index 022696e2..027dbd2e 100644 --- a/internal/domain/pet/view.go +++ b/internal/domain/pet/view.go @@ -48,6 +48,14 @@ func (pet *WithProfileImage) ToDetailView() *DetailView { } } +func ToDetailViewList(rows []databasegen.FindPetsBySOSPostIDRow) []DetailView { + pl := make([]DetailView, len(rows)) + for i, row := range rows { + pl[i] = *ToWithProfileImageFromSOSPostIDRow(row).ToDetailView() + } + return pl +} + type ListView struct { Pets []DetailView `json:"pets"` } diff --git a/internal/domain/pet/vmodel.go b/internal/domain/pet/vmodel.go deleted file mode 100644 index 8d453c27..00000000 --- a/internal/domain/pet/vmodel.go +++ /dev/null @@ -1,60 +0,0 @@ -package pet - -import ( - "github.com/pet-sitter/pets-next-door-api/internal/datatype" - "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" - "github.com/shopspring/decimal" -) - -type BasePet struct { - ID int `field:"id" json:"id"` - OwnerID int `field:"owner_id" json:"owner_id"` - Name string `field:"name" json:"name"` - PetType commonvo.PetType `field:"pet_type" json:"pet_type"` - Sex Gender `field:"sex" json:"sex"` - Neutered bool `field:"neutered" json:"neutered"` - Breed string `field:"breed" json:"breed"` - BirthDate datatype.Date `field:"birth_date" json:"birth_date"` - WeightInKg decimal.Decimal `field:"weight_in_kg" json:"weight_in_kg"` - Remarks string `field:"remarks" json:"remarks"` - CreatedAt string `field:"created_at" json:"created_at"` - UpdatedAt string `field:"updated_at" json:"updated_at"` - DeletedAt string `field:"deleted_at" json:"deleted_at"` -} - -type Pet struct { - BasePet - ProfileImageID *int `field:"profile_image_id"` -} - -type PetList []*Pet - -type ViewForSOSPost struct { - BasePet - ProfileImageURL *string `field:"profile_image_url" json:"profile_image_url"` -} - -func (v *ViewForSOSPost) ToDetailView() *DetailView { - return &DetailView{ - ID: int64(v.ID), - Name: v.Name, - PetType: v.PetType, - Sex: v.Sex, - Neutered: v.Neutered, - Breed: v.Breed, - BirthDate: v.BirthDate, - WeightInKg: v.WeightInKg, - Remarks: v.Remarks, - ProfileImageURL: v.ProfileImageURL, - } -} - -type ViewListForSOSPost []*ViewForSOSPost - -func (vl *ViewListForSOSPost) ToDetailViewList() []DetailView { - pl := make([]DetailView, len(*vl)) - for i, v := range *vl { - pl[i] = *v.ToDetailView() - } - return pl -} diff --git a/internal/domain/sospost/sos_post.go b/internal/domain/sospost/sos_post.go index 58d717bc..38721a2e 100644 --- a/internal/domain/sospost/sos_post.go +++ b/internal/domain/sospost/sos_post.go @@ -125,7 +125,7 @@ type SOSPostStore interface { FindSOSPostByID(ctx context.Context, tx *database.Tx, id int) (*SOSPost, *pnd.AppError) UpdateSOSPost(ctx context.Context, tx *database.Tx, request *UpdateSOSPostRequest) (*SOSPost, *pnd.AppError) FindConditionByID(ctx context.Context, tx *database.Tx, id int) (*ConditionList, *pnd.AppError) - FindPetsByID(ctx context.Context, tx *database.Tx, id int) (*pet.PetList, *pnd.AppError) + FindPetsByID(ctx context.Context, tx *database.Tx, id int) (*pet.ViewListForSOSPost, *pnd.AppError) WriteDates(ctx context.Context, tx *database.Tx, dates []string, sosPostID int) (*SOSDatesList, *pnd.AppError) FindDatesBySOSPostID(ctx context.Context, tx *database.Tx, sosPostID int) (*SOSDatesList, *pnd.AppError) } diff --git a/internal/infra/database/gen/pets.sql.go b/internal/infra/database/gen/pets.sql.go index 43036814..a4b9c614 100644 --- a/internal/infra/database/gen/pets.sql.go +++ b/internal/infra/database/gen/pets.sql.go @@ -335,6 +335,89 @@ func (q *Queries) FindPetsByIDs(ctx context.Context, arg FindPetsByIDsParams) ([ return items, nil } +const findPetsBySOSPostID = `-- name: FindPetsBySOSPostID :many +SELECT pets.id, + pets.owner_id, + pets.name, + pets.pet_type, + pets.sex, + pets.neutered, + pets.breed, + pets.birth_date, + pets.weight_in_kg, + pets.remarks, + media.url AS profile_image_url, + pets.created_at, + pets.updated_at, + pets.deleted_at +FROM pets + INNER JOIN + sos_posts_pets + ON + pets.id = sos_posts_pets.pet_id + LEFT JOIN + media + ON + pets.profile_image_id = media.id +WHERE sos_posts_pets.sos_post_id = $1 + AND sos_posts_pets.deleted_at IS NULL +` + +type FindPetsBySOSPostIDRow struct { + ID int32 + OwnerID int64 + Name string + PetType string + Sex string + Neutered bool + Breed string + BirthDate time.Time + WeightInKg string + Remarks string + ProfileImageUrl sql.NullString + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt sql.NullTime +} + +func (q *Queries) FindPetsBySOSPostID(ctx context.Context, sosPostID sql.NullInt64) ([]FindPetsBySOSPostIDRow, error) { + rows, err := q.db.QueryContext(ctx, findPetsBySOSPostID, sosPostID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindPetsBySOSPostIDRow + for rows.Next() { + var i FindPetsBySOSPostIDRow + if err := rows.Scan( + &i.ID, + &i.OwnerID, + &i.Name, + &i.PetType, + &i.Sex, + &i.Neutered, + &i.Breed, + &i.BirthDate, + &i.WeightInKg, + &i.Remarks, + &i.ProfileImageUrl, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); 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 +} + const updatePet = `-- name: UpdatePet :exec UPDATE pets diff --git a/internal/postgres/sos_post_store.go b/internal/postgres/sos_post_store.go index 5ba9b680..dc6530a3 100644 --- a/internal/postgres/sos_post_store.go +++ b/internal/postgres/sos_post_store.go @@ -11,7 +11,6 @@ import ( pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/domain/media" - "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/domain/sospost" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" ) @@ -742,73 +741,6 @@ func FindConditionByID(ctx context.Context, tx *database.Tx, id int) (*sospost.C return &conditions, nil } -func FindPetsByID(ctx context.Context, tx *database.Tx, id int) (*pet.ViewListForSOSPost, *pnd.AppError) { - const query = ` - SELECT - pets.id, - pets.owner_id, - pets.name, - pets.pet_type, - pets.sex, - pets.neutered, - pets.breed, - pets.birth_date, - pets.weight_in_kg, - pets.remarks, - pets.created_at, - pets.updated_at, - media.url AS profile_image_url - FROM - pets - INNER JOIN - sos_posts_pets - ON - pets.id = sos_posts_pets.pet_id - LEFT JOIN - media - ON - pets.profile_image_id = media.id - WHERE - sos_posts_pets.sos_post_id = $1 AND - sos_posts_pets.deleted_at IS NULL; - - ` - - pets := pet.ViewListForSOSPost{} - rows, err := tx.QueryContext(ctx, query, id) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - defer rows.Close() - - for rows.Next() { - petData := pet.ViewForSOSPost{} - if err := rows.Scan( - &petData.ID, - &petData.OwnerID, - &petData.Name, - &petData.PetType, - &petData.Sex, - &petData.Neutered, - &petData.Breed, - &petData.BirthDate, - &petData.WeightInKg, - &petData.Remarks, - &petData.CreatedAt, - &petData.UpdatedAt, - &petData.ProfileImageURL, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - pets = append(pets, &petData) - } - if err := rows.Err(); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return &pets, nil -} - func FindDatesBySOSPostID(ctx context.Context, tx *database.Tx, sosPostID int) (*sospost.SOSDatesList, *pnd.AppError) { const query = ` SELECT diff --git a/internal/service/sos_post_service.go b/internal/service/sos_post_service.go index 1e7f8d1c..000781ce 100644 --- a/internal/service/sos_post_service.go +++ b/internal/service/sos_post_service.go @@ -3,6 +3,8 @@ package service import ( "context" + "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" + utils "github.com/pet-sitter/pets-next-door-api/internal/common" "github.com/pet-sitter/pets-next-door-api/internal/domain/user" databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" @@ -56,9 +58,9 @@ func (service *SOSPostService) WriteSOSPost( return nil, err } - pets, err := postgres.FindPetsByID(ctx, tx, sosPost.ID) - if err != nil { - return nil, err + petRows, err2 := databasegen.New(tx).FindPetsBySOSPostID(ctx, utils.IntToNullInt64(sosPost.ID)) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } dates, err := postgres.FindDatesBySOSPostID(ctx, tx, sosPost.ID) @@ -73,7 +75,7 @@ func (service *SOSPostService) WriteSOSPost( return sosPost.ToWriteSOSPostView( mediaData.ToMediaViewList(), conditions.ToConditionViewList(), - pets.ToDetailViewList(), + pet.ToDetailViewList(petRows), dates.ToSOSDateViewList(), ), nil } @@ -223,9 +225,9 @@ func (service *SOSPostService) UpdateSOSPost( return nil, err } - pets, err := postgres.FindPetsByID(ctx, tx, updateSOSPost.ID) - if err != nil { - return nil, err + petRows, err2 := databasegen.New(tx).FindPetsBySOSPostID(ctx, utils.IntToNullInt64(updateSOSPost.ID)) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } dates, err := postgres.FindDatesBySOSPostID(ctx, tx, request.ID) @@ -240,7 +242,7 @@ func (service *SOSPostService) UpdateSOSPost( return updateSOSPost.ToUpdateSOSPostView( mediaData.ToMediaViewList(), conditions.ToConditionViewList(), - pets.ToDetailViewList(), + pet.ToDetailViewList(petRows), dates.ToSOSDateViewList(), ), nil } diff --git a/queries/pets.sql b/queries/pets.sql index e11f185f..2e0a7529 100644 --- a/queries/pets.sql +++ b/queries/pets.sql @@ -93,6 +93,33 @@ WHERE pets.id = ANY (sqlc.arg('ids')::int[]) (sqlc.arg('include_deleted')::boolean = FALSE AND pets.deleted_at IS NULL)) ORDER BY pets.created_at DESC; +-- name: FindPetsBySOSPostID :many +SELECT pets.id, + pets.owner_id, + pets.name, + pets.pet_type, + pets.sex, + pets.neutered, + pets.breed, + pets.birth_date, + pets.weight_in_kg, + pets.remarks, + media.url AS profile_image_url, + pets.created_at, + pets.updated_at, + pets.deleted_at +FROM pets + INNER JOIN + sos_posts_pets + ON + pets.id = sos_posts_pets.pet_id + LEFT JOIN + media + ON + pets.profile_image_id = media.id +WHERE sos_posts_pets.sos_post_id = $1 + AND sos_posts_pets.deleted_at IS NULL; + -- name: UpdatePet :exec UPDATE pets From 9346fc30f0500a402880949b9991f87659a91482 Mon Sep 17 00:00:00 2001 From: litsynp Date: Fri, 26 Apr 2024 00:07:29 +0900 Subject: [PATCH 09/12] fix: breed.BreedListView swagger reference --- cmd/server/handler/breed_handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/server/handler/breed_handler.go b/cmd/server/handler/breed_handler.go index 86053bc9..7cdab7a7 100644 --- a/cmd/server/handler/breed_handler.go +++ b/cmd/server/handler/breed_handler.go @@ -25,7 +25,7 @@ func NewBreedHandler(breedService service.BreedService) *BreedHandler { // @Param page query int false "페이지 번호" default(1) // @Param size query int false "페이지 사이즈" default(20) // @Param pet_type query string false "펫 종류" Enums(dog, cat) -// @Success 200 {object} pet.BreedListView +// @Success 200 {object} breed.BreedListView // @Router /breeds [get] func (h *BreedHandler) FindBreeds(c echo.Context) error { petType := pnd.ParseOptionalStringQuery(c, "pet_type") From 72549390b2dc4ecc4fd0ab264e181cc12d2c5288 Mon Sep 17 00:00:00 2001 From: litsynp Date: Fri, 26 Apr 2024 00:32:59 +0900 Subject: [PATCH 10/12] refactor: streamline and use sqlc for breed --- cmd/import_breeds/main.go | 69 ++++++------ cmd/server/handler/breed_handler.go | 10 +- internal/domain/breed/model.go | 37 ------- internal/domain/breed/params.go | 23 ++++ internal/domain/breed/view.go | 36 ++++--- internal/domain/commonvo/vo.go | 4 + internal/infra/database/gen/breeds.sql.go | 112 ++++++++++++++++++++ internal/postgres/breed_store.go | 123 ---------------------- internal/service/breed_service.go | 50 ++------- queries/breeds.sql | 20 ++++ 10 files changed, 229 insertions(+), 255 deletions(-) delete mode 100644 internal/domain/breed/model.go create mode 100644 internal/domain/breed/params.go create mode 100644 internal/infra/database/gen/breeds.sql.go delete mode 100644 internal/postgres/breed_store.go create mode 100644 queries/breeds.sql diff --git a/cmd/import_breeds/main.go b/cmd/import_breeds/main.go index 484f12fb..89b10436 100644 --- a/cmd/import_breeds/main.go +++ b/cmd/import_breeds/main.go @@ -2,11 +2,12 @@ package main import ( "context" - "database/sql" - "errors" "flag" "log" + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" + "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" @@ -15,7 +16,6 @@ import ( pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/configs" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - "github.com/pet-sitter/pets-next-door-api/internal/postgres" ) func main() { @@ -93,45 +93,48 @@ func parseFlags() Flags { func importBreed( ctx context.Context, conn *database.DB, petType commonvo.PetType, row breedsimporterservice.Row, -) (*breed.Breed, *pnd.AppError) { +) (*breed.DetailView, *pnd.AppError) { log.Printf("Importing breed with pet_type: %s, name: %s to database", petType, row.Breed) - var breedData *breed.Breed - err := database.WithTransaction(ctx, conn, func(tx *database.Tx) *pnd.AppError { - existing, err := postgres.FindBreedByPetTypeAndName(ctx, tx, petType, row.Breed) - if err != nil && !errors.Is(err.Err, sql.ErrNoRows) { - return err - } - - if existing != nil { - log.Printf( - "Breed with id: %d, pet_type: %s, name: %s already exists in database", - existing.ID, - existing.PetType, - existing.Name, - ) - breedData = existing - return nil - } - - breedData, err = postgres.CreateBreed(ctx, tx, &breed.Breed{PetType: petType, Name: row.Breed}) - if err != nil { - return err - } + existingList, err := databasegen.New(conn).FindBreeds(ctx, databasegen.FindBreedsParams{ + PetType: utils.StrToNullStr(petType.String()), + Name: utils.StrToNullStr(row.Breed), + }) + if err != nil { + return nil, pnd.FromPostgresError(err) + } + if len(existingList) > 1 { + existing := existingList[0] log.Printf( - "Succeeded to import breed with id: %d, pet_type: %s, name: %s to database", - breedData.ID, - breedData.PetType, - breedData.Name, + "Breed with id: %d, pet_type: %s, name: %s already exists in database", + existing.ID, + existing.PetType, + existing.Name, ) - return nil + return breed.ToDetailViewFromRows(existing), nil + } + + breedData, err := databasegen.New(conn).CreateBreed(ctx, databasegen.CreateBreedParams{ + Name: row.Breed, + PetType: petType.String(), }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - return breedData, nil + log.Printf( + "Succeeded to import breed with id: %d, pet_type: %s, name: %s to database", + breedData.ID, + breedData.PetType, + breedData.Name, + ) + + return &breed.DetailView{ + ID: int64(breedData.ID), + PetType: commonvo.PetType(breedData.PetType), + Name: breedData.Name, + }, nil } func importBreeds(ctx context.Context, conn *database.DB, petType commonvo.PetType, rows *[]breedsimporterservice.Row) { diff --git a/cmd/server/handler/breed_handler.go b/cmd/server/handler/breed_handler.go index 7cdab7a7..3d49fd91 100644 --- a/cmd/server/handler/breed_handler.go +++ b/cmd/server/handler/breed_handler.go @@ -3,6 +3,8 @@ package handler import ( "net/http" + "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" + "github.com/labstack/echo/v4" pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/service" @@ -25,7 +27,7 @@ func NewBreedHandler(breedService service.BreedService) *BreedHandler { // @Param page query int false "페이지 번호" default(1) // @Param size query int false "페이지 사이즈" default(20) // @Param pet_type query string false "펫 종류" Enums(dog, cat) -// @Success 200 {object} breed.BreedListView +// @Success 200 {object} breed.ListView // @Router /breeds [get] func (h *BreedHandler) FindBreeds(c echo.Context) error { petType := pnd.ParseOptionalStringQuery(c, "pet_type") @@ -34,7 +36,11 @@ func (h *BreedHandler) FindBreeds(c echo.Context) error { return c.JSON(err.StatusCode, err) } - res, err := h.breedService.FindBreeds(c.Request().Context(), page, size, petType) + res, err := h.breedService.FindBreeds(c.Request().Context(), &breed.FindBreedsParams{ + Page: page, + Size: size, + PetType: petType, + }) if err != nil { return c.JSON(err.StatusCode, err) } diff --git a/internal/domain/breed/model.go b/internal/domain/breed/model.go deleted file mode 100644 index 922223ea..00000000 --- a/internal/domain/breed/model.go +++ /dev/null @@ -1,37 +0,0 @@ -package breed - -import ( - "context" - - "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - - pnd "github.com/pet-sitter/pets-next-door-api/api" -) - -type Breed struct { - ID int `field:"id"` - Name string `field:"name"` - PetType commonvo.PetType `field:"pet_type"` - CreatedAt string `field:"created_at"` - UpdatedAt string `field:"updated_at"` - DeletedAt string `field:"deleted_at"` -} - -type BreedList struct { - *pnd.PaginatedView[Breed] -} - -func NewBreedList(page, size int) *BreedList { - return &BreedList{PaginatedView: pnd.NewPaginatedView( - page, size, false, make([]Breed, 0), - )} -} - -type BreedStore interface { - FindBreeds(ctx context.Context, tx *database.Tx, page, size int, petType *string) (*BreedList, *pnd.AppError) - FindBreedByPetTypeAndName( - ctx context.Context, tx *database.Tx, petType commonvo.PetType, name string, - ) (*Breed, *pnd.AppError) - CreateBreed(ctx context.Context, tx *database.Tx, breed *Breed) (*Breed, *pnd.AppError) -} diff --git a/internal/domain/breed/params.go b/internal/domain/breed/params.go new file mode 100644 index 00000000..78b9bafa --- /dev/null +++ b/internal/domain/breed/params.go @@ -0,0 +1,23 @@ +package breed + +import ( + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" +) + +type FindBreedsParams struct { + Page int + Size int + PetType *string + IncludeDeleted bool +} + +func (p *FindBreedsParams) ToDBParams() databasegen.FindBreedsParams { + pagination := utils.OffsetAndLimit(p.Page, p.Size) + return databasegen.FindBreedsParams{ + Limit: int32(pagination.Limit), + Offset: int32(pagination.Offset), + PetType: utils.StrPtrToNullStr(p.PetType), + IncludeDeleted: p.IncludeDeleted, + } +} diff --git a/internal/domain/breed/view.go b/internal/domain/breed/view.go index 448c6d6c..f0ffd8c9 100644 --- a/internal/domain/breed/view.go +++ b/internal/domain/breed/view.go @@ -3,31 +3,33 @@ package breed import ( pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" ) -type BreedView struct { - ID int `json:"id"` +type DetailView struct { + ID int64 `json:"id"` PetType commonvo.PetType `json:"petType"` Name string `json:"name"` } -type BreedListView struct { - *pnd.PaginatedView[*BreedView] +func ToDetailViewFromRows(row databasegen.FindBreedsRow) *DetailView { + return &DetailView{ + ID: int64(row.ID), + PetType: commonvo.PetType(row.PetType), + Name: row.Name, + } } -func (breeds *BreedList) ToBreedListView() *BreedListView { - breedViews := make([]*BreedView, len(breeds.Items)) - for i, breed := range breeds.Items { - breedViews[i] = &BreedView{ - ID: breed.ID, - PetType: breed.PetType, - Name: breed.Name, - } - } +type ListView struct { + *pnd.PaginatedView[*DetailView] +} - return &BreedListView{ - PaginatedView: pnd.NewPaginatedView( - breeds.Page, breeds.Size, breeds.IsLastPage, breedViews, - ), +func ToListViewFromRows(page, size int, rows []databasegen.FindBreedsRow) *ListView { + bl := &ListView{PaginatedView: pnd.NewPaginatedView(page, size, false, make([]*DetailView, len(rows)))} + for i, row := range rows { + bl.Items[i] = ToDetailViewFromRows(row) } + + bl.CalcLastPage() + return bl } diff --git a/internal/domain/commonvo/vo.go b/internal/domain/commonvo/vo.go index b45c17e0..4ceea6be 100644 --- a/internal/domain/commonvo/vo.go +++ b/internal/domain/commonvo/vo.go @@ -6,3 +6,7 @@ const ( PetTypeDog PetType = "dog" PetTypeCat PetType = "cat" ) + +func (p *PetType) String() string { + return string(*p) +} diff --git a/internal/infra/database/gen/breeds.sql.go b/internal/infra/database/gen/breeds.sql.go new file mode 100644 index 00000000..5db89a99 --- /dev/null +++ b/internal/infra/database/gen/breeds.sql.go @@ -0,0 +1,112 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: breeds.sql + +package databasegen + +import ( + "context" + "database/sql" + "time" +) + +const createBreed = `-- name: CreateBreed :one +INSERT INTO breeds (name, + pet_type, + created_at, + updated_at) +VALUES ($1, $2, NOW(), NOW()) +RETURNING id, pet_type, name, created_at, updated_at +` + +type CreateBreedParams struct { + Name string + PetType string +} + +type CreateBreedRow struct { + ID int32 + PetType string + Name string + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) CreateBreed(ctx context.Context, arg CreateBreedParams) (CreateBreedRow, error) { + row := q.db.QueryRowContext(ctx, createBreed, arg.Name, arg.PetType) + var i CreateBreedRow + err := row.Scan( + &i.ID, + &i.PetType, + &i.Name, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const findBreeds = `-- name: FindBreeds :many +SELECT id, + name, + pet_type, + created_at, + updated_at +FROM breeds +WHERE (pet_type = $3 OR $3 IS NULL) + AND (name = $4 OR $4 IS NULL) + AND (deleted_at IS NULL OR $5::boolean = TRUE) +ORDER BY id +LIMIT $1 OFFSET $2 +` + +type FindBreedsParams struct { + Limit int32 + Offset int32 + PetType sql.NullString + Name sql.NullString + IncludeDeleted bool +} + +type FindBreedsRow struct { + ID int32 + Name string + PetType string + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) FindBreeds(ctx context.Context, arg FindBreedsParams) ([]FindBreedsRow, error) { + rows, err := q.db.QueryContext(ctx, findBreeds, + arg.Limit, + arg.Offset, + arg.PetType, + arg.Name, + arg.IncludeDeleted, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindBreedsRow + for rows.Next() { + var i FindBreedsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.PetType, + &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 +} diff --git a/internal/postgres/breed_store.go b/internal/postgres/breed_store.go deleted file mode 100644 index c58529ae..00000000 --- a/internal/postgres/breed_store.go +++ /dev/null @@ -1,123 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" - "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" - - pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" -) - -func FindBreeds( - ctx context.Context, tx *database.Tx, page, size int, petType *string) (*breed.BreedList, *pnd.AppError, -) { - const sql = ` - SELECT - id, - name, - pet_type, - created_at, - updated_at - FROM - breeds - WHERE - (pet_type = $1 OR $1 IS NULL) AND - deleted_at IS NULL - ORDER BY id ASC - LIMIT $2 - OFFSET $3 - ` - - breedList := breed.NewBreedList(page, size) - rows, err := tx.QueryContext(ctx, sql, petType, size+1, (page-1)*size) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - defer rows.Close() - - for rows.Next() { - breedData := &breed.Breed{} - if err := rows.Scan( - &breedData.ID, &breedData.Name, &breedData.PetType, &breedData.CreatedAt, &breedData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - breedList.Items = append(breedList.Items, *breedData) - } - if err := rows.Err(); err != nil { - return nil, pnd.FromPostgresError(err) - } - - breedList.CalcLastPage() - return breedList, nil -} - -func FindBreedByPetTypeAndName( - ctx context.Context, tx *database.Tx, petType commonvo.PetType, name string, -) (*breed.Breed, *pnd.AppError) { - const sql = ` - SELECT - id, - name, - pet_type, - created_at, - updated_at - FROM - breeds - WHERE - pet_type = $1 AND - name = $2 AND - deleted_at IS NULL - ` - - breedData := &breed.Breed{} - if err := tx.QueryRowContext(ctx, sql, - petType, - name, - ).Scan( - &breedData.ID, - &breedData.Name, - &breedData.PetType, - &breedData.CreatedAt, - &breedData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return breedData, nil -} - -func CreateBreed(ctx context.Context, tx *database.Tx, breedData *breed.Breed) (*breed.Breed, *pnd.AppError) { - const sql = ` - INSERT INTO - breeds - ( - id, - name, - pet_type, - created_at, - updated_at - ) - VALUES - (DEFAULT, $1, $2, DEFAULT, DEFAULT) - RETURNING - id, pet_type, name, created_at, updated_at - ` - - if err := tx.QueryRowContext(ctx, sql, - breedData.Name, - breedData.PetType, - ).Scan( - &breedData.ID, - &breedData.PetType, - &breedData.Name, - &breedData.CreatedAt, - &breedData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return breedData, nil -} diff --git a/internal/service/breed_service.go b/internal/service/breed_service.go index ce7bc255..33ff663c 100644 --- a/internal/service/breed_service.go +++ b/internal/service/breed_service.go @@ -3,12 +3,11 @@ package service import ( "context" - "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" - "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" pnd "github.com/pet-sitter/pets-next-door-api/api" + "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - "github.com/pet-sitter/pets-next-door-api/internal/postgres" ) type BreedService struct { @@ -22,47 +21,12 @@ func NewBreedService(conn *database.DB) *BreedService { } func (s *BreedService) FindBreeds( - ctx context.Context, page, size int, petType *string, -) (*breed.BreedListView, *pnd.AppError) { - tx, err := s.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return nil, err - } - - breeds, err := postgres.FindBreeds(ctx, tx, page, size, petType) + ctx context.Context, params *breed.FindBreedsParams, +) (*breed.ListView, *pnd.AppError) { + rows, err := databasegen.New(s.conn).FindBreeds(ctx, params.ToDBParams()) if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return breeds.ToBreedListView(), nil -} - -func (s *BreedService) FindBreedByPetTypeAndName( - ctx context.Context, petType commonvo.PetType, name string, -) (*breed.BreedView, *pnd.AppError) { - tx, err := s.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return nil, err - } - - breedData, err := postgres.FindBreedByPetTypeAndName(ctx, tx, petType, name) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - return &breed.BreedView{ - ID: breedData.ID, - PetType: breedData.PetType, - Name: breedData.Name, - }, nil + return breed.ToListViewFromRows(params.Page, params.Size, rows), nil } diff --git a/queries/breeds.sql b/queries/breeds.sql new file mode 100644 index 00000000..58c0419e --- /dev/null +++ b/queries/breeds.sql @@ -0,0 +1,20 @@ +-- name: CreateBreed :one +INSERT INTO breeds (name, + pet_type, + created_at, + updated_at) +VALUES ($1, $2, NOW(), NOW()) +RETURNING id, pet_type, name, created_at, updated_at; + +-- name: FindBreeds :many +SELECT id, + name, + pet_type, + created_at, + updated_at +FROM breeds +WHERE (pet_type = sqlc.narg('pet_type') OR sqlc.narg('pet_type') IS NULL) + AND (name = sqlc.narg('name') OR sqlc.narg('name') IS NULL) + AND (deleted_at IS NULL OR sqlc.arg('include_deleted')::boolean = TRUE) +ORDER BY id +LIMIT $1 OFFSET $2; From 5cf6695ad4e6aea1e3ff03ba411859a8ce651f1d Mon Sep 17 00:00:00 2001 From: litsynp Date: Sat, 27 Apr 2024 01:16:12 +0900 Subject: [PATCH 11/12] refactor: streamline and use sqlc for media and resource media --- cmd/server/handler/media_handler.go | 12 +- internal/common/null.go | 7 ++ internal/domain/media/media.go | 31 ----- internal/domain/media/model.go | 22 ++++ internal/domain/media/resource_media.go | 35 ------ internal/domain/media/view.go | 80 ++++++++++--- internal/domain/pet/request.go | 4 +- internal/domain/resourcemedia/model.go | 11 ++ internal/domain/sospost/sos_post.go | 34 +++--- internal/domain/sospost/view.go | 74 ++++++------ internal/domain/user/request.go | 6 +- internal/infra/database/gen/media.sql.go | 86 ++++++++++++++ .../infra/database/gen/resource_media.sql.go | 112 ++++++++++++++++++ internal/postgres/media_store.go | 64 ---------- internal/postgres/resource_media_store.go | 97 --------------- internal/postgres/sos_post_store.go | 9 +- internal/service/media_service.go | 49 ++++---- internal/service/sos_post_service.go | 30 +++-- .../service/tests/sos_post_service_test.go | 34 +++--- internal/service/user_service.go | 11 +- internal/tests/factories.go | 6 +- internal/tests/service.go | 11 +- queries/media.sql | 19 +++ queries/resource_media.sql | 25 ++++ 24 files changed, 485 insertions(+), 384 deletions(-) delete mode 100644 internal/domain/media/media.go create mode 100644 internal/domain/media/model.go delete mode 100644 internal/domain/media/resource_media.go create mode 100644 internal/domain/resourcemedia/model.go create mode 100644 internal/infra/database/gen/media.sql.go create mode 100644 internal/infra/database/gen/resource_media.sql.go delete mode 100644 internal/postgres/media_store.go delete mode 100644 internal/postgres/resource_media_store.go create mode 100644 queries/media.sql create mode 100644 queries/resource_media.sql diff --git a/cmd/server/handler/media_handler.go b/cmd/server/handler/media_handler.go index 3d63e8c6..16191314 100644 --- a/cmd/server/handler/media_handler.go +++ b/cmd/server/handler/media_handler.go @@ -28,7 +28,7 @@ func NewMediaHandler(mediaService service.MediaService) *MediaHandler { // @Tags media // @Produce json // @Param id path int true "미디어 ID" -// @Success 200 {object} media.MediaView +// @Success 200 {object} media.DetailView // @Router /media/{id} [get] func (h *MediaHandler) FindMediaByID(c echo.Context) error { id, err := pnd.ParseIDFromPath(c, "id") @@ -36,12 +36,12 @@ func (h *MediaHandler) FindMediaByID(c echo.Context) error { return c.JSON(err.StatusCode, err) } - found, err := h.mediaService.FindMediaByID(c.Request().Context(), *id) + found, err := h.mediaService.FindMediaByID(c.Request().Context(), int64(*id)) if err != nil { return c.JSON(err.StatusCode, err) } - return c.JSON(http.StatusOK, found.ToMediaView()) + return c.JSON(http.StatusOK, found) } // UploadImage godoc @@ -51,7 +51,7 @@ func (h *MediaHandler) FindMediaByID(c echo.Context) error { // @Accept multipart/form-data // @Produce json // @Param file formData file true "이미지 파일" -// @Success 201 {object} media.MediaView +// @Success 201 {object} media.DetailView // @Router /media/images [post] func (h *MediaHandler) UploadImage(c echo.Context) error { fileHeader, err := c.FormFile("file") @@ -79,12 +79,12 @@ func (h *MediaHandler) UploadImage(c echo.Context) error { return c.JSON(pndErr.StatusCode, pndErr) } - res, err2 := h.mediaService.UploadMedia(c.Request().Context(), file, media.MediaTypeImage, fileHeader.Filename) + res, err2 := h.mediaService.UploadMedia(c.Request().Context(), file, media.TypeImage, fileHeader.Filename) if err2 != nil { return c.JSON(err2.StatusCode, err2) } - return c.JSON(http.StatusCreated, res.ToMediaView()) + return c.JSON(http.StatusCreated, res) } var supportedMimeTypes = []string{ diff --git a/internal/common/null.go b/internal/common/null.go index 212f0fc6..c07ff670 100644 --- a/internal/common/null.go +++ b/internal/common/null.go @@ -70,6 +70,13 @@ func IntToNullInt32(val int) sql.NullInt32 { } } +func Int64ToNullInt32(val int64) sql.NullInt32 { + return sql.NullInt32{ + Int32: int32(val), + Valid: val != 0, + } +} + func IntPtrToNullInt32(val *int) sql.NullInt32 { return sql.NullInt32{ Int32: int32(DerefOrEmpty(val)), diff --git a/internal/domain/media/media.go b/internal/domain/media/media.go deleted file mode 100644 index 4f852a02..00000000 --- a/internal/domain/media/media.go +++ /dev/null @@ -1,31 +0,0 @@ -package media - -import ( - "context" - - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - - pnd "github.com/pet-sitter/pets-next-door-api/api" -) - -type MediaType string - -const ( - MediaTypeImage MediaType = "image" -) - -type Media struct { - ID int `field:"id" json:"id"` - MediaType MediaType `field:"media_type" json:"media_type"` - URL string `field:"url" json:"url"` - CreatedAt string `field:"created_at" json:"created_at"` - UpdatedAt string `field:"updated_at" json:"updated_at"` - DeletedAt string `field:"deleted_at" json:"deleted_at"` -} - -type MediaList []*Media - -type MediaStore interface { - CreateMedia(ctx context.Context, tx *database.Tx, media *Media) (*Media, *pnd.AppError) - FindMediaByID(ctx context.Context, tx *database.Tx, id int) (*Media, *pnd.AppError) -} diff --git a/internal/domain/media/model.go b/internal/domain/media/model.go new file mode 100644 index 00000000..f24d80a7 --- /dev/null +++ b/internal/domain/media/model.go @@ -0,0 +1,22 @@ +package media + +type Type string + +const ( + TypeImage Type = "image" +) + +func (mt Type) String() string { + return string(mt) +} + +type ViewForSOSPost struct { + ID int64 `field:"id" json:"id"` + MediaType Type `field:"media_type" json:"media_type"` + URL string `field:"url" json:"url"` + CreatedAt string `field:"created_at" json:"created_at"` + UpdatedAt string `field:"updated_at" json:"updated_at"` + DeletedAt string `field:"deleted_at" json:"deleted_at"` +} + +type ViewListForSOSPost []*ViewForSOSPost diff --git a/internal/domain/media/resource_media.go b/internal/domain/media/resource_media.go deleted file mode 100644 index 3177c6ba..00000000 --- a/internal/domain/media/resource_media.go +++ /dev/null @@ -1,35 +0,0 @@ -package media - -import ( - "context" - "time" - - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - - pnd "github.com/pet-sitter/pets-next-door-api/api" -) - -type ResourceType string - -const ( - SOSResourceType ResourceType = "sos_posts" -) - -type ResourceMedia struct { - ID int `field:"id"` - ResourceType ResourceType `field:"resource_type"` - ResourceID int `field:"resource_id"` - MediaID int `field:"media_id"` - CreatedAt time.Time `field:"created_at"` - UpdatedAt time.Time `field:"updated_at"` - DeletedAt time.Time `field:"deleted_at"` -} - -type ResourceMediaStore interface { - CreateResourceMedia( - ctx context.Context, tx *database.Tx, resourceID, mediaID int, resourceType string, - ) (*ResourceMedia, *pnd.AppError) - FindResourceMediaByResourceID( - ctx context.Context, tx *database.Tx, resourceID int, resourceType string, - ) (*MediaList, *pnd.AppError) -} diff --git a/internal/domain/media/view.go b/internal/domain/media/view.go index c0c46dee..8e72f6e5 100644 --- a/internal/domain/media/view.go +++ b/internal/domain/media/view.go @@ -1,34 +1,80 @@ package media -type MediaView struct { - ID int `json:"id"` - MediaType MediaType `json:"mediaType"` +import ( + "time" + + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" +) + +type DetailView struct { + ID int64 `json:"id"` + MediaType Type `json:"mediaType"` URL string `json:"url"` - CreatedAt string `json:"createdAt"` + CreatedAt time.Time `json:"createdAt"` +} + +type ListView []*DetailView + +func ToDetailView(media databasegen.FindSingleMediaRow) *DetailView { + return &DetailView{ + ID: int64(media.ID), + MediaType: Type(media.MediaType), + URL: media.Url, + CreatedAt: media.CreatedAt, + } +} + +func ToDetailViewFromCreated(media databasegen.CreateMediaRow) *DetailView { + return &DetailView{ + ID: int64(media.ID), + MediaType: Type(media.MediaType), + URL: media.Url, + CreatedAt: media.CreatedAt, + } } -type MediaViewList []*MediaView +func ToDetailViewFromResourceMediaRows(resourceMedia databasegen.FindResourceMediaRow) *DetailView { + return &DetailView{ + ID: int64(resourceMedia.MediaID), + MediaType: Type(resourceMedia.MediaType), + URL: resourceMedia.Url, + CreatedAt: resourceMedia.CreatedAt, + } +} -func (media *Media) ToMediaView() *MediaView { - return &MediaView{ +func ToDetailViewFromViewForSOSPost(media ViewForSOSPost) *DetailView { + createdAt, err := time.Parse(time.RFC3339, media.CreatedAt) + if err != nil { + createdAt = time.Time{} + } + + return &DetailView{ ID: media.ID, MediaType: media.MediaType, URL: media.URL, - CreatedAt: media.CreatedAt, + CreatedAt: createdAt, } } -func (mediaList *MediaList) ToMediaViewList() MediaViewList { - mediaViewList := make(MediaViewList, len(*mediaList)) - for i, media := range *mediaList { - mediaViewList[i] = media.ToMediaView() +func ToListViewFromResourceMediaRows(resourceMediaList []databasegen.FindResourceMediaRow) ListView { + mediaViewList := make(ListView, len(resourceMediaList)) + for i, resourceMedia := range resourceMediaList { + mediaViewList[i] = ToDetailViewFromResourceMediaRows(resourceMedia) } return mediaViewList } -type ResourceMediaView struct { - ID int `field:"id"` - ResourceType ResourceType `field:"resource_type"` - ResourceID int `field:"resource_id"` - MediaID int `field:"media_id"` +func ToListViewFromViewListForSOSPost(mediaList ViewListForSOSPost) ListView { + mediaViewList := make(ListView, len(mediaList)) + for i, media := range mediaList { + mediaViewList[i] = ToDetailViewFromViewForSOSPost( + ViewForSOSPost{ + ID: media.ID, + MediaType: media.MediaType, + URL: media.URL, + CreatedAt: media.CreatedAt, + }, + ) + } + return mediaViewList } diff --git a/internal/domain/pet/request.go b/internal/domain/pet/request.go index 3438c486..af970079 100644 --- a/internal/domain/pet/request.go +++ b/internal/domain/pet/request.go @@ -19,7 +19,7 @@ type AddPetRequest struct { BirthDate utils.Date `json:"birthDate" validate:"required"` WeightInKg decimal.Decimal `json:"weightInKg" validate:"required"` Remarks string `json:"remarks"` - ProfileImageID *int `json:"profileImageId"` + ProfileImageID *int64 `json:"profileImageId"` } type UpdatePetRequest struct { @@ -29,5 +29,5 @@ type UpdatePetRequest struct { BirthDate utils.Date `json:"birthDate" validate:"required"` WeightInKg decimal.Decimal `json:"weightInKg" validate:"required"` Remarks string `json:"remarks"` - ProfileImageID *int `json:"profileImageId"` + ProfileImageID *int64 `json:"profileImageId"` } diff --git a/internal/domain/resourcemedia/model.go b/internal/domain/resourcemedia/model.go new file mode 100644 index 00000000..bcbb53b7 --- /dev/null +++ b/internal/domain/resourcemedia/model.go @@ -0,0 +1,11 @@ +package resourcemedia + +type ResourceType string + +const ( + SOSResourceType ResourceType = "sos_posts" +) + +func (r ResourceType) String() string { + return string(r) +} diff --git a/internal/domain/sospost/sos_post.go b/internal/domain/sospost/sos_post.go index 38721a2e..de0b4ecf 100644 --- a/internal/domain/sospost/sos_post.go +++ b/internal/domain/sospost/sos_post.go @@ -42,7 +42,7 @@ type SOSPost struct { CareType CareType `field:"care_type"` CarerGender CarerGender `field:"carer_gender"` RewardType RewardType `field:"reward_type"` - ThumbnailID int `field:"thumbnail_id"` + ThumbnailID int64 `field:"thumbnail_id"` CreatedAt time.Time `field:"created_at"` UpdatedAt time.Time `field:"updated_at"` DeletedAt time.Time `field:"deleted_at"` @@ -53,22 +53,22 @@ type SOSPostList struct { } type SOSPostInfo struct { - ID int `field:"id" json:"id"` - AuthorID int `field:"author" json:"author"` - Title string `field:"title" json:"title"` - Content string `field:"content" json:"content"` - Media media.MediaList `field:"media" json:"media"` - Conditions ConditionList `field:"conditions" json:"conditions"` - Pets pet.ViewListForSOSPost `field:"pets" json:"pets"` - Reward string `field:"reward" json:"reward"` - Dates SOSDatesList `field:"dates" json:"dates"` - CareType CareType `field:"careType" json:"careType"` - CarerGender CarerGender `field:"carerGender" json:"carerGender"` - RewardType RewardType `field:"rewardType" json:"rewardType"` - ThumbnailID int `field:"thumbnailId" json:"thumbnailId"` - CreatedAt time.Time `field:"createdAt" json:"createdAt"` - UpdatedAt time.Time `field:"updatedAt" json:"updatedAt"` - DeletedAt time.Time `field:"deletedAt" json:"deletedAt"` + ID int `field:"id" json:"id"` + AuthorID int `field:"author" json:"author"` + Title string `field:"title" json:"title"` + Content string `field:"content" json:"content"` + Media media.ViewListForSOSPost `field:"media" json:"media"` + Conditions ConditionList `field:"conditions" json:"conditions"` + Pets pet.ViewListForSOSPost `field:"pets" json:"pets"` + Reward string `field:"reward" json:"reward"` + Dates SOSDatesList `field:"dates" json:"dates"` + CareType CareType `field:"careType" json:"careType"` + CarerGender CarerGender `field:"carerGender" json:"carerGender"` + RewardType RewardType `field:"rewardType" json:"rewardType"` + ThumbnailID int64 `field:"thumbnailId" json:"thumbnailId"` + CreatedAt time.Time `field:"createdAt" json:"createdAt"` + UpdatedAt time.Time `field:"updatedAt" json:"updatedAt"` + DeletedAt time.Time `field:"deletedAt" json:"deletedAt"` } type SOSPostInfoList struct { diff --git a/internal/domain/sospost/view.go b/internal/domain/sospost/view.go index 0a11cb0e..85ba30f3 100644 --- a/internal/domain/sospost/view.go +++ b/internal/domain/sospost/view.go @@ -44,25 +44,25 @@ type WriteSOSPostRequest struct { } type WriteSOSPostView struct { - ID int `json:"id"` - AuthorID int `json:"authorId"` - Title string `json:"title"` - Content string `json:"content"` - Media media.MediaViewList `json:"media"` - Conditions []ConditionView `json:"conditions"` - Pets []pet.DetailView `json:"pets"` - Reward string `json:"reward"` - Dates []SOSDateView `json:"dates"` - CareType CareType `json:"careType"` - CarerGender CarerGender `json:"carerGender"` - RewardType RewardType `json:"rewardType"` - ThumbnailID int `json:"thumbnailId"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + ID int `json:"id"` + AuthorID int `json:"authorId"` + Title string `json:"title"` + Content string `json:"content"` + Media media.ListView `json:"media"` + Conditions []ConditionView `json:"conditions"` + Pets []pet.DetailView `json:"pets"` + Reward string `json:"reward"` + Dates []SOSDateView `json:"dates"` + CareType CareType `json:"careType"` + CarerGender CarerGender `json:"carerGender"` + RewardType RewardType `json:"rewardType"` + ThumbnailID int64 `json:"thumbnailId"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } func (p *SOSPost) ToWriteSOSPostView( - mediaList media.MediaViewList, + mediaList media.ListView, conditions []ConditionView, pets []pet.DetailView, sosDates []SOSDateView, @@ -91,7 +91,7 @@ type FindSOSPostView struct { Author *user.WithoutPrivateInfo `json:"author"` Title string `json:"title"` Content string `json:"content"` - Media media.MediaViewList `json:"media"` + Media media.ListView `json:"media"` Conditions []ConditionView `json:"conditions"` Pets []pet.DetailView `json:"pets"` Reward string `json:"reward"` @@ -99,14 +99,14 @@ type FindSOSPostView struct { CareType CareType `json:"careType"` CarerGender CarerGender `json:"carerGender"` RewardType RewardType `json:"rewardType"` - ThumbnailID int `json:"thumbnailId"` + ThumbnailID int64 `json:"thumbnailId"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } func (p *SOSPost) ToFindSOSPostView( author *user.WithoutPrivateInfo, - mediaList media.MediaViewList, + mediaList media.ListView, conditions []ConditionView, pets []pet.DetailView, sosDates []SOSDateView, @@ -152,7 +152,7 @@ func FromEmptySOSPostInfoList(sosPosts *SOSPostInfoList) *FindSOSPostListView { func (p *SOSPostInfo) ToFindSOSPostInfoView( author *user.WithoutPrivateInfo, - mediaList media.MediaViewList, + mediaList media.ListView, conditions []ConditionView, pets []pet.DetailView, sosDates []SOSDateView, @@ -180,7 +180,7 @@ type UpdateSOSPostRequest struct { ID int `json:"id" validate:"required"` Title string `json:"title" validate:"required"` Content string `json:"content" validate:"required"` - ImageIDs []int `json:"imageIds" validate:"required"` + ImageIDs []int64 `json:"imageIds" validate:"required"` Dates []SOSDateView `json:"dates" validate:"required"` Reward string `json:"reward"` CareType CareType `json:"careType" validate:"required,oneof=foster visiting"` @@ -191,25 +191,25 @@ type UpdateSOSPostRequest struct { } type UpdateSOSPostView struct { - ID int `json:"id"` - AuthorID int `json:"authorId"` - Title string `json:"title"` - Content string `json:"content"` - Media media.MediaViewList `json:"media"` - Conditions []ConditionView `json:"conditions"` - Pets []pet.DetailView `json:"pets"` - Reward string `json:"reward"` - Dates []SOSDateView `json:"dates"` - CareType CareType `json:"careType"` - CarerGender CarerGender `json:"carerGender"` - RewardType RewardType `json:"rewardType"` - ThumbnailID int `json:"thumbnailId"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + ID int `json:"id"` + AuthorID int `json:"authorId"` + Title string `json:"title"` + Content string `json:"content"` + Media media.ListView `json:"media"` + Conditions []ConditionView `json:"conditions"` + Pets []pet.DetailView `json:"pets"` + Reward string `json:"reward"` + Dates []SOSDateView `json:"dates"` + CareType CareType `json:"careType"` + CarerGender CarerGender `json:"carerGender"` + RewardType RewardType `json:"rewardType"` + ThumbnailID int64 `json:"thumbnailId"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } func (p *SOSPost) ToUpdateSOSPostView( - mediaList media.MediaViewList, + mediaList media.ListView, conditions []ConditionView, pets []pet.DetailView, sosDates []SOSDateView, diff --git a/internal/domain/user/request.go b/internal/domain/user/request.go index f35e56b0..66497eea 100644 --- a/internal/domain/user/request.go +++ b/internal/domain/user/request.go @@ -9,7 +9,7 @@ type RegisterUserRequest struct { Email string `json:"email" validate:"required,email"` Nickname string `json:"nickname" validate:"required"` Fullname string `json:"fullname" validate:"required"` - ProfileImageID *int `json:"profileImageId"` + ProfileImageID *int64 `json:"profileImageId"` FirebaseProviderType FirebaseProviderType `json:"fbProviderType" validate:"required"` FirebaseUID string `json:"fbUid" validate:"required"` } @@ -20,7 +20,7 @@ func (r *RegisterUserRequest) ToDBParams() databasegen.CreateUserParams { Nickname: r.Nickname, Fullname: r.Fullname, Password: "", - ProfileImageID: utils.IntPtrToNullInt64(r.ProfileImageID), + ProfileImageID: utils.Int64PtrToNullInt64(r.ProfileImageID), FbProviderType: r.FirebaseProviderType.NullString(), FbUid: utils.StrToNullStr(r.FirebaseUID), } @@ -36,5 +36,5 @@ type UserStatusRequest struct { type UpdateUserRequest struct { Nickname string `json:"nickname" validate:"required"` - ProfileImageID *int `json:"profileImageId" validate:"omitempty"` + ProfileImageID *int64 `json:"profileImageId" validate:"omitempty"` } diff --git a/internal/infra/database/gen/media.sql.go b/internal/infra/database/gen/media.sql.go new file mode 100644 index 00000000..fb78fbe3 --- /dev/null +++ b/internal/infra/database/gen/media.sql.go @@ -0,0 +1,86 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: media.sql + +package databasegen + +import ( + "context" + "database/sql" + "time" +) + +const createMedia = `-- name: CreateMedia :one +INSERT INTO media +(media_type, + url, + created_at, + updated_at) +VALUES ($1, $2, NOW(), NOW()) +RETURNING id, media_type, url, created_at, updated_at +` + +type CreateMediaParams struct { + MediaType string + Url string +} + +type CreateMediaRow struct { + ID int32 + MediaType string + Url string + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) CreateMedia(ctx context.Context, arg CreateMediaParams) (CreateMediaRow, error) { + row := q.db.QueryRowContext(ctx, createMedia, arg.MediaType, arg.Url) + var i CreateMediaRow + err := row.Scan( + &i.ID, + &i.MediaType, + &i.Url, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const findSingleMedia = `-- name: FindSingleMedia :one +SELECT id, + media_type, + url, + created_at, + updated_at +FROM media +WHERE (id = $1 OR $1 IS NULL) + AND ($2::BOOLEAN = TRUE OR + ($2::BOOLEAN = FALSE AND deleted_at IS NULL)) +` + +type FindSingleMediaParams struct { + ID sql.NullInt32 + IncludeDeleted bool +} + +type FindSingleMediaRow struct { + ID int32 + MediaType string + Url string + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) FindSingleMedia(ctx context.Context, arg FindSingleMediaParams) (FindSingleMediaRow, error) { + row := q.db.QueryRowContext(ctx, findSingleMedia, arg.ID, arg.IncludeDeleted) + var i FindSingleMediaRow + err := row.Scan( + &i.ID, + &i.MediaType, + &i.Url, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/internal/infra/database/gen/resource_media.sql.go b/internal/infra/database/gen/resource_media.sql.go new file mode 100644 index 00000000..09a2b251 --- /dev/null +++ b/internal/infra/database/gen/resource_media.sql.go @@ -0,0 +1,112 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: resource_media.sql + +package databasegen + +import ( + "context" + "database/sql" + "time" +) + +const createResourceMedia = `-- name: CreateResourceMedia :one +INSERT INTO resource_media +(resource_id, + media_id, + resource_type, + created_at, + updated_at) +VALUES ($1, $2, $3, NOW(), NOW()) +RETURNING id, resource_id, media_id, resource_type, created_at, updated_at +` + +type CreateResourceMediaParams struct { + ResourceID sql.NullInt64 + MediaID sql.NullInt64 + ResourceType sql.NullString +} + +type CreateResourceMediaRow struct { + ID int32 + ResourceID sql.NullInt64 + MediaID sql.NullInt64 + ResourceType sql.NullString + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) CreateResourceMedia(ctx context.Context, arg CreateResourceMediaParams) (CreateResourceMediaRow, error) { + row := q.db.QueryRowContext(ctx, createResourceMedia, arg.ResourceID, arg.MediaID, arg.ResourceType) + var i CreateResourceMediaRow + err := row.Scan( + &i.ID, + &i.ResourceID, + &i.MediaID, + &i.ResourceType, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const findResourceMedia = `-- name: FindResourceMedia :many +SELECT m.id AS media_id, + m.media_type, + m.url, + m.created_at, + m.updated_at +FROM resource_media rm + INNER JOIN + media m + ON + rm.media_id = m.id +WHERE (rm.resource_id = $1 OR $1 IS NULL) + AND (rm.resource_type = $2 OR $2 IS NULL) + AND ($3::BOOLEAN = TRUE OR + ($3::BOOLEAN = FALSE AND rm.deleted_at IS NULL)) +` + +type FindResourceMediaParams struct { + ResourceID sql.NullInt64 + ResourceType sql.NullString + IncludeDeleted bool +} + +type FindResourceMediaRow struct { + MediaID int32 + MediaType string + Url string + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) FindResourceMedia(ctx context.Context, arg FindResourceMediaParams) ([]FindResourceMediaRow, error) { + rows, err := q.db.QueryContext(ctx, findResourceMedia, arg.ResourceID, arg.ResourceType, arg.IncludeDeleted) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindResourceMediaRow + for rows.Next() { + var i FindResourceMediaRow + if err := rows.Scan( + &i.MediaID, + &i.MediaType, + &i.Url, + &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 +} diff --git a/internal/postgres/media_store.go b/internal/postgres/media_store.go deleted file mode 100644 index 4ee4b289..00000000 --- a/internal/postgres/media_store.go +++ /dev/null @@ -1,64 +0,0 @@ -package postgres - -import ( - "context" - - pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/domain/media" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" -) - -func CreateMedia(ctx context.Context, tx *database.Tx, mediaData *media.Media) (*media.Media, *pnd.AppError) { - const sql = ` - INSERT INTO - media - ( - media_type, - url, - created_at, - updated_at - ) - VALUES ($1, $2, NOW(), NOW()) - RETURNING id, created_at, updated_at - ` - - if err := tx.QueryRowContext(ctx, sql, - mediaData.MediaType, - mediaData.URL, - ).Scan(&mediaData.ID, &mediaData.CreatedAt, &mediaData.UpdatedAt); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return mediaData, nil -} - -func FindMediaByID(ctx context.Context, tx *database.Tx, id int) (*media.Media, *pnd.AppError) { - const sql = ` - SELECT - id, - media_type, - url, - created_at, - updated_at - FROM - media - WHERE - id = $1 AND - deleted_at IS NULL - ` - - mediaData := &media.Media{} - if err := tx.QueryRowContext(ctx, sql, - id, - ).Scan( - &mediaData.ID, - &mediaData.MediaType, - &mediaData.URL, - &mediaData.CreatedAt, - &mediaData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return mediaData, nil -} diff --git a/internal/postgres/resource_media_store.go b/internal/postgres/resource_media_store.go deleted file mode 100644 index 0e9ea271..00000000 --- a/internal/postgres/resource_media_store.go +++ /dev/null @@ -1,97 +0,0 @@ -package postgres - -import ( - "context" - - pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/domain/media" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" -) - -func CreateResourceMedia( - ctx context.Context, tx *database.Tx, resourceID, mediaID int, resourceType string, -) (*media.ResourceMedia, *pnd.AppError) { - const sql = ` - INSERT INTO - resource_media - ( - resource_id, - media_id, - resource_type, - created_at, - updated_at - ) - VALUES ($1, $2, $3, NOW(), NOW()) - RETURNING id, resource_id, media_id, created_at, updated_at - ` - - resourceMedia := &media.ResourceMedia{} - err := tx.QueryRowContext(ctx, sql, - resourceID, - mediaID, - resourceType, - ).Scan( - &resourceMedia.ID, - &resourceMedia.ResourceID, - &resourceMedia.MediaID, - &resourceMedia.CreatedAt, - &resourceMedia.UpdatedAt, - ) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - - return resourceMedia, nil -} - -func FindResourceMediaByResourceID( - ctx context.Context, tx *database.Tx, resourceID int, resourceType string, -) (*media.MediaList, *pnd.AppError) { - const sql = ` - SELECT - m.id, - m.media_type, - m.url, - m.created_at, - m.updated_at - FROM - resource_media rm - INNER JOIN - media m - ON - rm.media_id = m.id - WHERE - rm.resource_id = $1 AND - rm.resource_type = $2 AND - rm.deleted_at IS NULL - ` - - var mediaList media.MediaList - rows, err := tx.QueryContext(ctx, sql, - resourceID, - resourceType, - ) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - defer rows.Close() - - for rows.Next() { - mediaItem := media.Media{} - if err := rows.Scan( - &mediaItem.ID, - &mediaItem.MediaType, - &mediaItem.URL, - &mediaItem.CreatedAt, - &mediaItem.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - mediaList = append(mediaList, &mediaItem) - } - if err := rows.Err(); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return &mediaList, nil -} diff --git a/internal/postgres/sos_post_store.go b/internal/postgres/sos_post_store.go index dc6530a3..91391a31 100644 --- a/internal/postgres/sos_post_store.go +++ b/internal/postgres/sos_post_store.go @@ -7,10 +7,11 @@ import ( "fmt" "time" + "github.com/pet-sitter/pets-next-door-api/internal/domain/resourcemedia" + utils "github.com/pet-sitter/pets-next-door-api/internal/common" pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/domain/media" "github.com/pet-sitter/pets-next-door-api/internal/domain/sospost" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" ) @@ -134,7 +135,7 @@ func WriteSOSPost( VALUES ($1, $2, $3, NOW(), NOW())`, imageID, sosPost.ID, - media.SOSResourceType, + resourcemedia.SOSResourceType, ); err != nil { return nil, pnd.FromPostgresError(err) } @@ -590,7 +591,7 @@ func updateSOSPostsDates(ctx context.Context, tx *database.Tx, postID int, dates return nil } -func updateSOSPostsMedia(ctx context.Context, tx *database.Tx, postID int, mediaIDs []int) *pnd.AppError { +func updateSOSPostsMedia(ctx context.Context, tx *database.Tx, postID int, mediaIDs []int64) *pnd.AppError { if _, err := tx.ExecContext(ctx, ` UPDATE resource_media @@ -618,7 +619,7 @@ func updateSOSPostsMedia(ctx context.Context, tx *database.Tx, postID int, media `, mediaID, postID, - media.SOSResourceType, + resourcemedia.SOSResourceType, ); err != nil { return pnd.FromPostgresError(err) } diff --git a/internal/service/media_service.go b/internal/service/media_service.go index 2191ad03..6a0b8202 100644 --- a/internal/service/media_service.go +++ b/internal/service/media_service.go @@ -5,13 +5,15 @@ import ( "io" "path/filepath" + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" + "github.com/aws/aws-sdk-go/private/protocol/rest" "github.com/google/uuid" pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/domain/media" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" s3infra "github.com/pet-sitter/pets-next-door-api/internal/infra/s3" - "github.com/pet-sitter/pets-next-door-api/internal/postgres" ) type MediaService struct { @@ -36,8 +38,8 @@ type UploadFileView struct { } func (s *MediaService) UploadMedia( - ctx context.Context, file io.ReadSeeker, mediaType media.MediaType, fileName string, -) (*media.Media, *pnd.AppError) { + ctx context.Context, file io.ReadSeeker, mediaType media.Type, fileName string, +) (*media.DetailView, *pnd.AppError) { randomFileName := generateRandomFileName(fileName) fullPath := "media/" + randomFileName @@ -51,10 +53,7 @@ func (s *MediaService) UploadMedia( return nil, pnd.ErrUnknown(err) } - created, err := s.CreateMedia(ctx, &media.Media{ - MediaType: mediaType, - URL: req.HTTPRequest.URL.String(), - }) + created, err := s.CreateMedia(ctx, mediaType, req.HTTPRequest.URL.String()) if err != nil { return nil, err } @@ -62,40 +61,36 @@ func (s *MediaService) UploadMedia( return created, nil } -func (s *MediaService) CreateMedia(ctx context.Context, mediaData *media.Media) (*media.Media, *pnd.AppError) { +func (s *MediaService) CreateMedia( + ctx context.Context, mediaType media.Type, url string, +) (*media.DetailView, *pnd.AppError) { tx, err := s.conn.BeginTx(ctx) defer tx.Rollback() if err != nil { return nil, err } - created, err := postgres.CreateMedia(ctx, tx, mediaData) - if err != nil { - return nil, err + created, err2 := databasegen.New(s.conn).CreateMedia(ctx, databasegen.CreateMediaParams{ + MediaType: mediaType.String(), + Url: url, + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } if err := tx.Commit(); err != nil { return nil, err } - - return created, nil + return media.ToDetailViewFromCreated(created), nil } -func (s *MediaService) FindMediaByID(ctx context.Context, id int) (*media.Media, *pnd.AppError) { - tx, err := s.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return nil, err - } - - mediaData, err := postgres.FindMediaByID(ctx, tx, id) +func (s *MediaService) FindMediaByID(ctx context.Context, id int64) (*media.DetailView, *pnd.AppError) { + mediaData, err := databasegen.New(s.conn).FindSingleMedia(ctx, databasegen.FindSingleMediaParams{ + ID: utils.Int64ToNullInt32(id), + }) if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - return mediaData, nil + return media.ToDetailView(mediaData), nil } diff --git a/internal/service/sos_post_service.go b/internal/service/sos_post_service.go index 000781ce..f8953e64 100644 --- a/internal/service/sos_post_service.go +++ b/internal/service/sos_post_service.go @@ -3,6 +3,8 @@ package service import ( "context" + "github.com/pet-sitter/pets-next-door-api/internal/domain/resourcemedia" + "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" utils "github.com/pet-sitter/pets-next-door-api/internal/common" @@ -48,9 +50,12 @@ func (service *SOSPostService) WriteSOSPost( return nil, err } - mediaData, err := postgres.FindResourceMediaByResourceID(ctx, tx, sosPost.ID, string(media.SOSResourceType)) - if err != nil { - return nil, err + mediaData, err2 := databasegen.New(tx).FindResourceMedia(ctx, databasegen.FindResourceMediaParams{ + ResourceID: utils.IntToNullInt64(sosPost.ID), + ResourceType: utils.StrToNullStr(resourcemedia.SOSResourceType.String()), + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } conditions, err := postgres.FindConditionByID(ctx, tx, sosPost.ID) @@ -73,7 +78,7 @@ func (service *SOSPostService) WriteSOSPost( } return sosPost.ToWriteSOSPostView( - mediaData.ToMediaViewList(), + media.ToListViewFromResourceMediaRows(mediaData), conditions.ToConditionViewList(), pet.ToDetailViewList(petRows), dates.ToSOSDateViewList(), @@ -111,7 +116,7 @@ func (service *SOSPostService) FindSOSPosts( Nickname: author.Nickname, ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), }, - sosPost.Media.ToMediaViewList(), + media.ToListViewFromViewListForSOSPost(sosPost.Media), sosPost.Conditions.ToConditionViewList(), sosPost.Pets.ToDetailViewList(), sosPost.Dates.ToSOSDateViewList(), @@ -153,7 +158,7 @@ func (service *SOSPostService) FindSOSPostsByAuthorID( Nickname: author.Nickname, ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), }, - sosPost.Media.ToMediaViewList(), + media.ToListViewFromViewListForSOSPost(sosPost.Media), sosPost.Conditions.ToConditionViewList(), sosPost.Pets.ToDetailViewList(), sosPost.Dates.ToSOSDateViewList(), @@ -194,7 +199,7 @@ func (service *SOSPostService) FindSOSPostByID(ctx context.Context, id int) (*so Nickname: author.Nickname, ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), }, - sosPost.Media.ToMediaViewList(), + media.ToListViewFromViewListForSOSPost(sosPost.Media), sosPost.Conditions.ToConditionViewList(), sosPost.Pets.ToDetailViewList(), sosPost.Dates.ToSOSDateViewList(), @@ -215,9 +220,12 @@ func (service *SOSPostService) UpdateSOSPost( return nil, err } - mediaData, err := postgres.FindResourceMediaByResourceID(ctx, tx, updateSOSPost.ID, string(media.SOSResourceType)) - if err != nil { - return nil, err + mediaData, err2 := databasegen.New(tx).FindResourceMedia(ctx, databasegen.FindResourceMediaParams{ + ResourceID: utils.IntToNullInt64(updateSOSPost.ID), + ResourceType: utils.StrToNullStr(resourcemedia.SOSResourceType.String()), + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } conditions, err := postgres.FindConditionByID(ctx, tx, updateSOSPost.ID) @@ -240,7 +248,7 @@ func (service *SOSPostService) UpdateSOSPost( } return updateSOSPost.ToUpdateSOSPostView( - mediaData.ToMediaViewList(), + media.ToListViewFromResourceMediaRows(mediaData), conditions.ToConditionViewList(), pet.ToDetailViewList(petRows), dates.ToSOSDateViewList(), diff --git a/internal/service/tests/sos_post_service_test.go b/internal/service/tests/sos_post_service_test.go index 6978a69d..447b64cf 100644 --- a/internal/service/tests/sos_post_service_test.go +++ b/internal/service/tests/sos_post_service_test.go @@ -55,7 +55,7 @@ func TestSOSPostService(t *testing.T) { // when sosPostService := service.NewSOSPostService(db) - imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} + imageIDs := []int64{sosPostImage.ID, sosPostImage2.ID} petIDs := []int64{addPets.ID} sosPostData := tests.GenerateDummyWriteSOSPostRequest(imageIDs, petIDs, 0) @@ -67,7 +67,7 @@ func TestSOSPostService(t *testing.T) { // then assertConditionEquals(t, sosPost.Conditions, sosPostData.ConditionIDs) assertPetEquals(t, sosPost.Pets[0], *addPets) - assertMediaEquals(t, sosPost.Media, (&media.MediaList{sosPostImage, sosPostImage2}).ToMediaViewList()) + assertMediaEquals(t, sosPost.Media, media.ListView{sosPostImage, sosPostImage2}) assertDatesEquals(t, sosPost.Dates, sosPostData.Dates) if sosPost.Title != sosPostData.Title { @@ -88,7 +88,7 @@ func TestSOSPostService(t *testing.T) { if sosPost.RewardType != sosPostData.RewardType { t.Errorf("got %v want %v", sosPost.RewardType, sosPostData.RewardType) } - if int64(sosPost.ThumbnailID) != sosPostData.ImageIDs[0] { + if sosPost.ThumbnailID != sosPostData.ImageIDs[0] { t.Errorf("got %v want %v", sosPost.ThumbnailID, sosPostData.ImageIDs[0]) } if int64(sosPost.AuthorID) != owner.ID { @@ -120,7 +120,7 @@ func TestSOSPostService(t *testing.T) { addPets := tests.AddDummyPet(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} + imageIDs := []int64{sosPostImage.ID, sosPostImage2.ID} petIDs := []int64{addPets.ID} conditionIDs := []int{1, 2} @@ -141,7 +141,7 @@ func TestSOSPostService(t *testing.T) { idx := len(sosPostList.Items) - i - 1 assertConditionEquals(t, sosPost.Conditions, conditionIDs) assertPetEquals(t, sosPost.Pets[0], *addPets) - assertMediaEquals(t, sosPost.Media, (&media.MediaList{sosPostImage, sosPostImage2}).ToMediaViewList()) + assertMediaEquals(t, sosPost.Media, media.ListView{sosPostImage, sosPostImage2}) assertAuthorEquals(t, sosPost.Author, author) assertDatesEquals(t, sosPost.Dates, sosPosts[idx].Dates) assertFindSOSPostEquals(t, sosPost, sosPosts[idx]) @@ -170,7 +170,7 @@ func TestSOSPostService(t *testing.T) { addPets := tests.AddDummyPets(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} + imageIDs := []int64{sosPostImage.ID, sosPostImage2.ID} conditionIDs := []int{1, 2} var sosPosts []sospost.WriteSOSPostView @@ -190,7 +190,7 @@ func TestSOSPostService(t *testing.T) { idx := len(sosPostList.Items) - i - 1 assertConditionEquals(t, sosPost.Conditions, conditionIDs) assertPetEquals(t, sosPost.Pets[i-1], addPets.Pets[i-1]) - assertMediaEquals(t, sosPost.Media, (&media.MediaList{sosPostImage, sosPostImage2}).ToMediaViewList()) + assertMediaEquals(t, sosPost.Media, media.ListView{sosPostImage, sosPostImage2}) assertAuthorEquals(t, sosPost.Author, author) assertDatesEquals(t, sosPost.Dates, sosPosts[idx].Dates) assertFindSOSPostEquals(t, sosPost, sosPosts[idx]) @@ -219,7 +219,7 @@ func TestSOSPostService(t *testing.T) { addPets := tests.AddDummyPets(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} + imageIDs := []int64{sosPostImage.ID, sosPostImage2.ID} conditionIDs := []int{1, 2} var sosPosts []sospost.WriteSOSPostView @@ -252,7 +252,7 @@ func TestSOSPostService(t *testing.T) { idx := len(sosPostList.Items) - i - 1 assertConditionEquals(t, sosPost.Conditions, conditionIDs) assertPetEquals(t, sosPost.Pets[i-1], addPets.Pets[i-1]) - assertMediaEquals(t, sosPost.Media, (&media.MediaList{sosPostImage, sosPostImage2}).ToMediaViewList()) + assertMediaEquals(t, sosPost.Media, media.ListView{sosPostImage, sosPostImage2}) assertAuthorEquals(t, sosPost.Author, author) assertDatesEquals(t, sosPost.Dates, sosPosts[idx].Dates) assertFindSOSPostEquals(t, sosPost, sosPosts[idx]) @@ -280,7 +280,7 @@ func TestSOSPostService(t *testing.T) { addPet := tests.AddDummyPet(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} + imageIDs := []int64{sosPostImage.ID, sosPostImage2.ID} conditionIDs := []int{1, 2} sosPosts := make([]sospost.WriteSOSPostView, 0) @@ -300,7 +300,7 @@ func TestSOSPostService(t *testing.T) { idx := len(sosPostListByAuthorID.Items) - i - 1 assertConditionEquals(t, sosPost.Conditions, conditionIDs) assertPetEquals(t, sosPost.Pets[0], *addPet) - assertMediaEquals(t, sosPost.Media, (&media.MediaList{sosPostImage, sosPostImage2}).ToMediaViewList()) + assertMediaEquals(t, sosPost.Media, media.ListView{sosPostImage, sosPostImage2}) assertAuthorEquals(t, sosPost.Author, author) assertDatesEquals(t, sosPost.Dates, sosPosts[idx].Dates) assertFindSOSPostEquals(t, sosPost, sosPosts[idx]) @@ -330,7 +330,7 @@ func TestSOSPostService(t *testing.T) { addPet := tests.AddDummyPet(t, ctx, userService, uid, &profileImage.ID) sosPostService := service.NewSOSPostService(db) - imageIDs := []int64{int64(sosPostImage.ID), int64(sosPostImage2.ID)} + imageIDs := []int64{sosPostImage.ID, sosPostImage2.ID} conditionIDs := []int{1, 2} sosPosts := make([]sospost.WriteSOSPostView, 0) @@ -348,7 +348,7 @@ func TestSOSPostService(t *testing.T) { // then assertConditionEquals(t, sosPosts[0].Conditions, conditionIDs) assertPetEquals(t, sosPosts[0].Pets[0], *addPet) - assertMediaEquals(t, findSOSPostByID.Media, (&media.MediaList{sosPostImage, sosPostImage2}).ToMediaViewList()) + assertMediaEquals(t, findSOSPostByID.Media, media.ListView{sosPostImage, sosPostImage2}) assertAuthorEquals(t, findSOSPostByID.Author, author) assertDatesEquals(t, findSOSPostByID.Dates, sosPosts[0].Dates) assertFindSOSPostEquals(t, *findSOSPostByID, sosPosts[0]) @@ -375,7 +375,7 @@ func TestSOSPostService(t *testing.T) { sosPostService := service.NewSOSPostService(db) sosPost := tests.WriteDummySOSPosts(t, ctx, - sosPostService, uid, []int64{int64(sosPostImage.ID)}, []int64{addPet.ID}, + sosPostService, uid, []int64{sosPostImage.ID}, []int64{addPet.ID}, 1, ) @@ -383,7 +383,7 @@ func TestSOSPostService(t *testing.T) { ID: sosPost.ID, Title: "Title2", Content: "Content2", - ImageIDs: []int{sosPostImage.ID, sosPostImage2.ID}, + ImageIDs: []int64{sosPostImage.ID, sosPostImage2.ID}, Reward: "Reward2", Dates: []sospost.SOSDateView{ {"2024-04-10", "2024-04-20"}, @@ -404,7 +404,7 @@ func TestSOSPostService(t *testing.T) { assertConditionEquals(t, sosPost.Conditions, updateSOSPostData.ConditionIDs) assertPetEquals(t, sosPost.Pets[0], *addPet) - assertMediaEquals(t, updateSOSPost.Media, (&media.MediaList{sosPostImage, sosPostImage2}).ToMediaViewList()) + assertMediaEquals(t, updateSOSPost.Media, media.ListView{sosPostImage, sosPostImage2}) assertDatesEquals(t, updateSOSPost.Dates, updateSOSPostData.Dates) if updateSOSPost.Title != updateSOSPostData.Title { @@ -520,7 +520,7 @@ func assertPetEquals(t *testing.T, got, want pet.DetailView) { } } -func assertMediaEquals(t *testing.T, got, want media.MediaViewList) { +func assertMediaEquals(t *testing.T, got, want media.ListView) { t.Helper() for i, mediaData := range want { diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 02e324c9..8359733d 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -13,7 +13,6 @@ import ( "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/domain/user" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - "github.com/pet-sitter/pets-next-door-api/internal/postgres" ) type UserService struct { @@ -96,7 +95,7 @@ func (service *UserService) ExistsByNickname(ctx context.Context, nickname strin } func (service *UserService) UpdateUserByUID( - ctx context.Context, uid, nickname string, profileImageID *int, + ctx context.Context, uid, nickname string, profileImageID *int64, ) (*user.MyProfileView, *pnd.AppError) { tx, err := service.conn.BeginTx(ctx) defer tx.Rollback() @@ -106,7 +105,7 @@ func (service *UserService) UpdateUserByUID( _, err2 := databasegen.New(service.conn).WithTx(tx.Tx).UpdateUserByFbUID(ctx, databasegen.UpdateUserByFbUIDParams{ Nickname: nickname, - ProfileImageID: utils.IntPtrToNullInt64(profileImageID), + ProfileImageID: utils.Int64PtrToNullInt64(profileImageID), FbUid: utils.StrToNullStr(uid), }) if err2 != nil { @@ -181,7 +180,7 @@ func (service *UserService) AddPetsToOwner( // 프로필 이미지 ID가 DB에 존재하는지 확인 for _, item := range addPetsRequest.Pets { if item.ProfileImageID != nil { - if _, err := postgres.FindMediaByID(ctx, tx, *item.ProfileImageID); err != nil { + if _, err := service.mediaService.FindMediaByID(ctx, *item.ProfileImageID); err != nil { return nil, pnd.ErrInvalidBody(fmt.Errorf("존재하지 않는 프로필 이미지 ID입니다. ID: %d", *item.ProfileImageID)) } } @@ -205,7 +204,7 @@ func (service *UserService) AddPetsToOwner( BirthDate: birthDate, WeightInKg: item.WeightInKg.String(), Remarks: item.Remarks, - ProfileImageID: utils.IntPtrToNullInt64(item.ProfileImageID), + ProfileImageID: utils.Int64PtrToNullInt64(item.ProfileImageID), } row, err := databasegen.New(service.conn).WithTx(tx.Tx).CreatePet(ctx, petToCreate) if err != nil { @@ -270,7 +269,7 @@ func (service *UserService) UpdatePet( BirthDate: birthDate, WeightInKg: updatePetRequest.WeightInKg.String(), Remarks: updatePetRequest.Remarks, - ProfileImageID: utils.IntPtrToNullInt64(updatePetRequest.ProfileImageID), + ProfileImageID: utils.Int64PtrToNullInt64(updatePetRequest.ProfileImageID), }); err != nil { return nil, pnd.FromPostgresError(err) } diff --git a/internal/tests/factories.go b/internal/tests/factories.go index e8cbda45..e1bf1c43 100644 --- a/internal/tests/factories.go +++ b/internal/tests/factories.go @@ -10,7 +10,7 @@ import ( "github.com/shopspring/decimal" ) -func GenerateDummyRegisterUserRequest(profileImageID *int) *user.RegisterUserRequest { +func GenerateDummyRegisterUserRequest(profileImageID *int64) *user.RegisterUserRequest { return &user.RegisterUserRequest{ Email: "test@example.com", Nickname: "nickname", @@ -21,7 +21,7 @@ func GenerateDummyRegisterUserRequest(profileImageID *int) *user.RegisterUserReq } } -func GenerateDummyAddPetRequest(profileImageID *int) *pet.AddPetRequest { +func GenerateDummyAddPetRequest(profileImageID *int64) *pet.AddPetRequest { birthDate, _ := datatype.ParseDate("2020-01-01") return &pet.AddPetRequest{ Name: "name", @@ -35,7 +35,7 @@ func GenerateDummyAddPetRequest(profileImageID *int) *pet.AddPetRequest { } } -func GenerateDummyAddPetsRequest(profileImageID *int) []pet.AddPetRequest { +func GenerateDummyAddPetsRequest(profileImageID *int64) []pet.AddPetRequest { birthDate1, _ := datatype.ParseDate("2020-01-01") birthDate2, _ := datatype.ParseDate("2020-02-01") birthDate3, _ := datatype.ParseDate("2020-03-01") diff --git a/internal/tests/service.go b/internal/tests/service.go index 309f5093..6af6809e 100644 --- a/internal/tests/service.go +++ b/internal/tests/service.go @@ -11,12 +11,9 @@ import ( "github.com/pet-sitter/pets-next-door-api/internal/service" ) -func AddDummyMedia(t *testing.T, ctx context.Context, mediaService *service.MediaService) *media.Media { +func AddDummyMedia(t *testing.T, ctx context.Context, mediaService *service.MediaService) *media.DetailView { t.Helper() - mediaData, err := mediaService.CreateMedia(ctx, &media.Media{ - MediaType: media.MediaTypeImage, - URL: "http://example.com", - }) + mediaData, err := mediaService.CreateMedia(ctx, media.TypeImage, "http://example.com") if err != nil { t.Errorf("got %v want %v", err, nil) } @@ -46,7 +43,7 @@ func AddDummyPet( ctx context.Context, userService *service.UserService, ownerUID string, - profileImageID *int, + profileImageID *int64, ) *pet.DetailView { t.Helper() petList, err := userService.AddPetsToOwner(ctx, ownerUID, pet.AddPetsToOwnerRequest{ @@ -64,7 +61,7 @@ func AddDummyPets( ctx context.Context, userService *service.UserService, ownerUID string, - profileImageID *int, + profileImageID *int64, ) pet.ListView { t.Helper() petList, err := userService.AddPetsToOwner(ctx, ownerUID, pet.AddPetsToOwnerRequest{ diff --git a/queries/media.sql b/queries/media.sql new file mode 100644 index 00000000..27ca3b58 --- /dev/null +++ b/queries/media.sql @@ -0,0 +1,19 @@ +-- name: CreateMedia :one +INSERT INTO media +(media_type, + url, + created_at, + updated_at) +VALUES ($1, $2, NOW(), NOW()) +RETURNING id, media_type, url, created_at, updated_at; + +-- name: FindSingleMedia :one +SELECT id, + media_type, + url, + created_at, + updated_at +FROM media +WHERE (id = sqlc.narg('id') OR sqlc.narg('id') IS NULL) + AND (sqlc.arg('include_deleted')::BOOLEAN = TRUE OR + (sqlc.arg('include_deleted')::BOOLEAN = FALSE AND deleted_at IS NULL)); diff --git a/queries/resource_media.sql b/queries/resource_media.sql new file mode 100644 index 00000000..29a0733b --- /dev/null +++ b/queries/resource_media.sql @@ -0,0 +1,25 @@ +-- name: CreateResourceMedia :one +INSERT INTO resource_media +(resource_id, + media_id, + resource_type, + created_at, + updated_at) +VALUES ($1, $2, $3, NOW(), NOW()) +RETURNING id, resource_id, media_id, resource_type, created_at, updated_at; + +-- name: FindResourceMedia :many +SELECT m.id AS media_id, + m.media_type, + m.url, + m.created_at, + m.updated_at +FROM resource_media rm + INNER JOIN + media m + ON + rm.media_id = m.id +WHERE (rm.resource_id = sqlc.narg('resource_id') OR sqlc.narg('resource_id') IS NULL) + AND (rm.resource_type = sqlc.narg('resource_type') OR sqlc.narg('resource_type') IS NULL) + AND (sqlc.arg('include_deleted')::BOOLEAN = TRUE OR + (sqlc.arg('include_deleted')::BOOLEAN = FALSE AND rm.deleted_at IS NULL)); From 50fd3d6e44f6ed8f6de4a62d1846552fd91bce90 Mon Sep 17 00:00:00 2001 From: litsynp Date: Sat, 27 Apr 2024 02:35:07 +0900 Subject: [PATCH 12/12] refactor: streamline and use sqlc for SOS condition and SOS post condition --- cmd/import_conditions/main.go | 27 ++-- cmd/server/handler/condition_handler.go | 6 +- cmd/server/router.go | 2 +- internal/common/null.go | 4 + internal/domain/soscondition/model.go | 25 ++++ internal/domain/soscondition/view.go | 73 +++++++++ internal/domain/sospost/condition.go | 34 ----- .../domain/sospost/{sos_post.go => model.go} | 36 ++--- internal/domain/sospost/view.go | 93 +++++------- .../infra/database/gen/sos_conditions.sql.go | 138 ++++++++++++++++++ internal/postgres/condition_store.go | 67 --------- internal/postgres/sos_post_store.go | 8 +- internal/service/condition_service.go | 39 ----- internal/service/sos_condition_service.go | 59 ++++++++ internal/service/sos_post_service.go | 28 ++-- .../service/tests/sos_post_service_test.go | 16 +- queries/sos_conditions.sql | 35 +++++ 17 files changed, 433 insertions(+), 257 deletions(-) create mode 100644 internal/domain/soscondition/model.go create mode 100644 internal/domain/soscondition/view.go delete mode 100644 internal/domain/sospost/condition.go rename internal/domain/sospost/{sos_post.go => model.go} (70%) create mode 100644 internal/infra/database/gen/sos_conditions.sql.go delete mode 100644 internal/postgres/condition_store.go delete mode 100644 internal/service/condition_service.go create mode 100644 internal/service/sos_condition_service.go create mode 100644 queries/sos_conditions.sql diff --git a/cmd/import_conditions/main.go b/cmd/import_conditions/main.go index e8c7c66e..0ef1baaf 100644 --- a/cmd/import_conditions/main.go +++ b/cmd/import_conditions/main.go @@ -4,11 +4,10 @@ import ( "context" "log" - pnd "github.com/pet-sitter/pets-next-door-api/api" + "github.com/pet-sitter/pets-next-door-api/internal/service" + "github.com/pet-sitter/pets-next-door-api/internal/configs" - "github.com/pet-sitter/pets-next-door-api/internal/domain/sospost" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - "github.com/pet-sitter/pets-next-door-api/internal/postgres" ) func main() { @@ -19,22 +18,18 @@ func main() { log.Fatalf("error opening database: %v\n", err) } - var result string - var err2 *pnd.AppError - ctx := context.Background() - err2 = database.WithTransaction(ctx, db, func(tx *database.Tx) *pnd.AppError { - result, err2 = postgres.InitConditions(ctx, tx, sospost.ConditionName) - if err2 != nil { - return err2 - } - - return nil - }) + conditionService := service.NewSOSConditionService(db) + conditionList, err2 := conditionService.InitConditions(ctx) if err2 != nil { - log.Fatalf("error initializing condition: %v\n", err2) + log.Fatalf("error initializing conditions: %v\n", err2) + } + + log.Println("Total conditions imported: ", len(conditionList)) + for _, condition := range conditionList { + log.Println("Condition ID: ", condition.ID, "Condition Name: ", condition.Name) } - log.Println(result) + log.Println("Finished importing condition") } diff --git a/cmd/server/handler/condition_handler.go b/cmd/server/handler/condition_handler.go index e2a7046c..c56565b5 100644 --- a/cmd/server/handler/condition_handler.go +++ b/cmd/server/handler/condition_handler.go @@ -9,10 +9,10 @@ import ( ) type ConditionHandler struct { - conditionService service.ConditionService + conditionService service.SOSConditionService } -func NewConditionHandler(conditionService service.ConditionService) *ConditionHandler { +func NewConditionHandler(conditionService service.SOSConditionService) *ConditionHandler { return &ConditionHandler{conditionService: conditionService} } @@ -22,7 +22,7 @@ func NewConditionHandler(conditionService service.ConditionService) *ConditionHa // @Tags posts // @Accept json // @Produce json -// @Success 200 {object} []sospost.ConditionView +// @Success 200 {object} soscondition.ListView // @Router /posts/sos/conditions [get] func (h *ConditionHandler) FindConditions(c echo.Context) error { res, err := h.conditionService.FindConditions(c.Request().Context()) diff --git a/cmd/server/router.go b/cmd/server/router.go index ed21af33..d5a4db36 100644 --- a/cmd/server/router.go +++ b/cmd/server/router.go @@ -53,7 +53,7 @@ func NewRouter(app *firebaseinfra.FirebaseApp) (*echo.Echo, error) { authService := service.NewFirebaseBearerAuthService(authClient, userService) breedService := service.NewBreedService(db) sosPostService := service.NewSOSPostService(db) - conditionService := service.NewConditionService(db) + conditionService := service.NewSOSConditionService(db) // Initialize handlers authHandler := handler.NewAuthHandler(authService, kakaoinfra.NewKakaoDefaultClient()) diff --git a/internal/common/null.go b/internal/common/null.go index c07ff670..a01b36dd 100644 --- a/internal/common/null.go +++ b/internal/common/null.go @@ -21,6 +21,10 @@ func NullStrToStrPtr(val sql.NullString) *string { return nil } +func NullStrToStr(val sql.NullString) string { + return val.String +} + func StrPtrToNullStr(val *string) sql.NullString { return sql.NullString{ String: DerefOrEmpty(val), diff --git a/internal/domain/soscondition/model.go b/internal/domain/soscondition/model.go new file mode 100644 index 00000000..c788582b --- /dev/null +++ b/internal/domain/soscondition/model.go @@ -0,0 +1,25 @@ +package soscondition + +type ViewForSOSPost struct { + ID int `field:"id"` + Name string `field:"name"` + CreatedAt string `field:"created_at"` + UpdatedAt string `field:"update_at"` + DeletedAt string `field:"deleted_at"` +} + +type ViewListForSOSPost []*ViewForSOSPost + +type AvailableName string + +const ( + CCTVPermission AvailableName = "CCTV, 펫캠 촬영 동의" + IDVerification AvailableName = "신분증 인증" + PhonePermission AvailableName = "사전 통화 가능 여부" +) + +func (c AvailableName) String() string { + return string(c) +} + +var AvailableNames = []AvailableName{CCTVPermission, IDVerification, PhonePermission} diff --git a/internal/domain/soscondition/view.go b/internal/domain/soscondition/view.go new file mode 100644 index 00000000..2d2c0a99 --- /dev/null +++ b/internal/domain/soscondition/view.go @@ -0,0 +1,73 @@ +package soscondition + +import ( + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" +) + +type DetailView struct { + ID int64 `json:"id"` + Name string `json:"name"` +} + +func ToDetailView(row databasegen.SosCondition) *DetailView { + return &DetailView{ + ID: int64(row.ID), + Name: utils.NullStrToStr(row.Name), + } +} + +func ToDetailViewFromRows(row databasegen.SosCondition) *DetailView { + return &DetailView{ + ID: int64(row.ID), + Name: utils.NullStrToStr(row.Name), + } +} + +func ToDetailViewFromSOSPostCondition(row databasegen.FindSOSPostConditionsRow) *DetailView { + return &DetailView{ + ID: int64(row.ID), + Name: utils.NullStrToStr(row.Name), + } +} + +func ToDetailViewFromViewForSOSPost(view ViewForSOSPost) *DetailView { + return &DetailView{ + ID: int64(view.ID), + Name: view.Name, + } +} + +type ListView []*DetailView + +func ToListView(row []databasegen.SosCondition) ListView { + conditionViews := make(ListView, len(row)) + for i, condition := range row { + conditionViews[i] = ToDetailView(condition) + } + return conditionViews +} + +func ToListViewFromRows(rows []databasegen.SosCondition) ListView { + conditionViews := make(ListView, len(rows)) + for i, row := range rows { + conditionViews[i] = ToDetailViewFromRows(row) + } + return conditionViews +} + +func ToListViewFromSOSPostConditions(rows []databasegen.FindSOSPostConditionsRow) ListView { + conditionViews := make(ListView, len(rows)) + for i, row := range rows { + conditionViews[i] = ToDetailViewFromSOSPostCondition(row) + } + return conditionViews +} + +func ToListViewFromViewForSOSPost(views ViewListForSOSPost) ListView { + conditionViews := make(ListView, len(views)) + for i, view := range views { + conditionViews[i] = ToDetailViewFromViewForSOSPost(*view) + } + return conditionViews +} diff --git a/internal/domain/sospost/condition.go b/internal/domain/sospost/condition.go deleted file mode 100644 index dc36b87b..00000000 --- a/internal/domain/sospost/condition.go +++ /dev/null @@ -1,34 +0,0 @@ -package sospost - -import ( - "context" - - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - - pnd "github.com/pet-sitter/pets-next-door-api/api" -) - -type Condition struct { - ID int `field:"id"` - Name string `field:"name"` - CreatedAt string `field:"created_at"` - UpdatedAt string `field:"update_at"` - DeletedAt string `field:"deleted_at"` -} - -type ConditionList []*Condition - -type SOSCondition string - -const ( - CCTVPermission SOSCondition = "CCTV, 펫캠 촬영 동의" - IDVerification SOSCondition = "신분증 인증" - PhonePermission SOSCondition = "사전 통화 가능 여부" -) - -var ConditionName = []SOSCondition{CCTVPermission, IDVerification, PhonePermission} - -type ConditionStore interface { - InitConditions(ctx context.Context, tx *database.Tx, conditions []SOSCondition) (string, *pnd.AppError) - FindConditions(ctx context.Context, tx *database.Tx) (*ConditionList, *pnd.AppError) -} diff --git a/internal/domain/sospost/sos_post.go b/internal/domain/sospost/model.go similarity index 70% rename from internal/domain/sospost/sos_post.go rename to internal/domain/sospost/model.go index de0b4ecf..6929f14d 100644 --- a/internal/domain/sospost/sos_post.go +++ b/internal/domain/sospost/model.go @@ -4,6 +4,8 @@ import ( "context" "time" + "github.com/pet-sitter/pets-next-door-api/internal/domain/soscondition" + pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/domain/media" "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" @@ -53,22 +55,22 @@ type SOSPostList struct { } type SOSPostInfo struct { - ID int `field:"id" json:"id"` - AuthorID int `field:"author" json:"author"` - Title string `field:"title" json:"title"` - Content string `field:"content" json:"content"` - Media media.ViewListForSOSPost `field:"media" json:"media"` - Conditions ConditionList `field:"conditions" json:"conditions"` - Pets pet.ViewListForSOSPost `field:"pets" json:"pets"` - Reward string `field:"reward" json:"reward"` - Dates SOSDatesList `field:"dates" json:"dates"` - CareType CareType `field:"careType" json:"careType"` - CarerGender CarerGender `field:"carerGender" json:"carerGender"` - RewardType RewardType `field:"rewardType" json:"rewardType"` - ThumbnailID int64 `field:"thumbnailId" json:"thumbnailId"` - CreatedAt time.Time `field:"createdAt" json:"createdAt"` - UpdatedAt time.Time `field:"updatedAt" json:"updatedAt"` - DeletedAt time.Time `field:"deletedAt" json:"deletedAt"` + ID int `field:"id" json:"id"` + AuthorID int `field:"author" json:"author"` + Title string `field:"title" json:"title"` + Content string `field:"content" json:"content"` + Media media.ViewListForSOSPost `field:"media" json:"media"` + Conditions soscondition.ViewListForSOSPost `field:"conditions" json:"conditions"` + Pets pet.ViewListForSOSPost `field:"pets" json:"pets"` + Reward string `field:"reward" json:"reward"` + Dates SOSDatesList `field:"dates" json:"dates"` + CareType CareType `field:"careType" json:"careType"` + CarerGender CarerGender `field:"carerGender" json:"carerGender"` + RewardType RewardType `field:"rewardType" json:"rewardType"` + ThumbnailID int64 `field:"thumbnailId" json:"thumbnailId"` + CreatedAt time.Time `field:"createdAt" json:"createdAt"` + UpdatedAt time.Time `field:"updatedAt" json:"updatedAt"` + DeletedAt time.Time `field:"deletedAt" json:"deletedAt"` } type SOSPostInfoList struct { @@ -124,7 +126,7 @@ type SOSPostStore interface { ) (*SOSPostInfoList, *pnd.AppError) FindSOSPostByID(ctx context.Context, tx *database.Tx, id int) (*SOSPost, *pnd.AppError) UpdateSOSPost(ctx context.Context, tx *database.Tx, request *UpdateSOSPostRequest) (*SOSPost, *pnd.AppError) - FindConditionByID(ctx context.Context, tx *database.Tx, id int) (*ConditionList, *pnd.AppError) + FindConditionByID(ctx context.Context, tx *database.Tx, id int) (*soscondition.ViewListForSOSPost, *pnd.AppError) FindPetsByID(ctx context.Context, tx *database.Tx, id int) (*pet.ViewListForSOSPost, *pnd.AppError) WriteDates(ctx context.Context, tx *database.Tx, dates []string, sosPostID int) (*SOSDatesList, *pnd.AppError) FindDatesBySOSPostID(ctx context.Context, tx *database.Tx, sosPostID int) (*SOSDatesList, *pnd.AppError) diff --git a/internal/domain/sospost/view.go b/internal/domain/sospost/view.go index 85ba30f3..6cc63833 100644 --- a/internal/domain/sospost/view.go +++ b/internal/domain/sospost/view.go @@ -5,31 +5,10 @@ import ( utils "github.com/pet-sitter/pets-next-door-api/internal/common" "github.com/pet-sitter/pets-next-door-api/internal/domain/media" "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" + "github.com/pet-sitter/pets-next-door-api/internal/domain/soscondition" "github.com/pet-sitter/pets-next-door-api/internal/domain/user" ) -type ConditionView struct { - ID int `json:"id"` - Name string `json:"name"` -} - -type ConditionViewList []*ConditionView - -func (c *Condition) ToConditionView() *ConditionView { - return &ConditionView{ - ID: c.ID, - Name: c.Name, - } -} - -func (cl *ConditionList) ToConditionViewList() []ConditionView { - conditionViews := make([]ConditionView, len(*cl)) - for i, c := range *cl { - conditionViews[i] = *c.ToConditionView() - } - return conditionViews -} - type WriteSOSPostRequest struct { Title string `json:"title" validate:"required"` Content string `json:"content" validate:"required"` @@ -44,26 +23,26 @@ type WriteSOSPostRequest struct { } type WriteSOSPostView struct { - ID int `json:"id"` - AuthorID int `json:"authorId"` - Title string `json:"title"` - Content string `json:"content"` - Media media.ListView `json:"media"` - Conditions []ConditionView `json:"conditions"` - Pets []pet.DetailView `json:"pets"` - Reward string `json:"reward"` - Dates []SOSDateView `json:"dates"` - CareType CareType `json:"careType"` - CarerGender CarerGender `json:"carerGender"` - RewardType RewardType `json:"rewardType"` - ThumbnailID int64 `json:"thumbnailId"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + ID int `json:"id"` + AuthorID int `json:"authorId"` + Title string `json:"title"` + Content string `json:"content"` + Media media.ListView `json:"media"` + Conditions soscondition.ListView `json:"conditions"` + Pets []pet.DetailView `json:"pets"` + Reward string `json:"reward"` + Dates []SOSDateView `json:"dates"` + CareType CareType `json:"careType"` + CarerGender CarerGender `json:"carerGender"` + RewardType RewardType `json:"rewardType"` + ThumbnailID int64 `json:"thumbnailId"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } func (p *SOSPost) ToWriteSOSPostView( mediaList media.ListView, - conditions []ConditionView, + conditions soscondition.ListView, pets []pet.DetailView, sosDates []SOSDateView, ) *WriteSOSPostView { @@ -92,7 +71,7 @@ type FindSOSPostView struct { Title string `json:"title"` Content string `json:"content"` Media media.ListView `json:"media"` - Conditions []ConditionView `json:"conditions"` + Conditions soscondition.ListView `json:"conditions"` Pets []pet.DetailView `json:"pets"` Reward string `json:"reward"` Dates []SOSDateView `json:"dates"` @@ -107,7 +86,7 @@ type FindSOSPostView struct { func (p *SOSPost) ToFindSOSPostView( author *user.WithoutPrivateInfo, mediaList media.ListView, - conditions []ConditionView, + conditions soscondition.ListView, pets []pet.DetailView, sosDates []SOSDateView, ) *FindSOSPostView { @@ -153,7 +132,7 @@ func FromEmptySOSPostInfoList(sosPosts *SOSPostInfoList) *FindSOSPostListView { func (p *SOSPostInfo) ToFindSOSPostInfoView( author *user.WithoutPrivateInfo, mediaList media.ListView, - conditions []ConditionView, + conditions soscondition.ListView, pets []pet.DetailView, sosDates []SOSDateView, ) *FindSOSPostView { @@ -191,26 +170,26 @@ type UpdateSOSPostRequest struct { } type UpdateSOSPostView struct { - ID int `json:"id"` - AuthorID int `json:"authorId"` - Title string `json:"title"` - Content string `json:"content"` - Media media.ListView `json:"media"` - Conditions []ConditionView `json:"conditions"` - Pets []pet.DetailView `json:"pets"` - Reward string `json:"reward"` - Dates []SOSDateView `json:"dates"` - CareType CareType `json:"careType"` - CarerGender CarerGender `json:"carerGender"` - RewardType RewardType `json:"rewardType"` - ThumbnailID int64 `json:"thumbnailId"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + ID int `json:"id"` + AuthorID int `json:"authorId"` + Title string `json:"title"` + Content string `json:"content"` + Media media.ListView `json:"media"` + Conditions soscondition.ListView `json:"conditions"` + Pets []pet.DetailView `json:"pets"` + Reward string `json:"reward"` + Dates []SOSDateView `json:"dates"` + CareType CareType `json:"careType"` + CarerGender CarerGender `json:"carerGender"` + RewardType RewardType `json:"rewardType"` + ThumbnailID int64 `json:"thumbnailId"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } func (p *SOSPost) ToUpdateSOSPostView( mediaList media.ListView, - conditions []ConditionView, + conditions soscondition.ListView, pets []pet.DetailView, sosDates []SOSDateView, ) *UpdateSOSPostView { diff --git a/internal/infra/database/gen/sos_conditions.sql.go b/internal/infra/database/gen/sos_conditions.sql.go new file mode 100644 index 00000000..55d7dc8f --- /dev/null +++ b/internal/infra/database/gen/sos_conditions.sql.go @@ -0,0 +1,138 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: sos_conditions.sql + +package databasegen + +import ( + "context" + "database/sql" + "time" +) + +const createSOSCondition = `-- name: CreateSOSCondition :one +INSERT INTO sos_conditions +(id, + name, + created_at, + updated_at) +SELECT $1, $2, now(), now() +WHERE NOT EXISTS (SELECT 1 + FROM sos_conditions + WHERE name = $2::VARCHAR(50)) +RETURNING id, name, created_at, updated_at, deleted_at +` + +type CreateSOSConditionParams struct { + ID int32 + Name sql.NullString +} + +func (q *Queries) CreateSOSCondition(ctx context.Context, arg CreateSOSConditionParams) (SosCondition, error) { + row := q.db.QueryRowContext(ctx, createSOSCondition, arg.ID, arg.Name) + var i SosCondition + err := row.Scan( + &i.ID, + &i.Name, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const findConditions = `-- name: FindConditions :many +SELECT id, + name, + created_at, + updated_at, + deleted_at +FROM sos_conditions +WHERE ($1::BOOLEAN = TRUE OR + ($1::BOOLEAN = FALSE AND deleted_at IS NULL)) +` + +func (q *Queries) FindConditions(ctx context.Context, includeDeleted bool) ([]SosCondition, error) { + rows, err := q.db.QueryContext(ctx, findConditions, includeDeleted) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SosCondition + for rows.Next() { + var i SosCondition + if err := rows.Scan( + &i.ID, + &i.Name, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); 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 +} + +const findSOSPostConditions = `-- name: FindSOSPostConditions :many +SELECT sos_conditions.id, + sos_conditions.name, + sos_conditions.created_at, + sos_conditions.updated_at +FROM sos_conditions + INNER JOIN + sos_posts_conditions + ON + sos_conditions.id = sos_posts_conditions.sos_condition_id +WHERE sos_posts_conditions.sos_post_id = $1 + AND ($2::BOOLEAN = TRUE OR + ($2::BOOLEAN = FALSE AND sos_posts_conditions.deleted_at IS NULL)) +` + +type FindSOSPostConditionsParams struct { + SosPostID sql.NullInt64 + IncludeDeleted bool +} + +type FindSOSPostConditionsRow struct { + ID int32 + Name sql.NullString + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) FindSOSPostConditions(ctx context.Context, arg FindSOSPostConditionsParams) ([]FindSOSPostConditionsRow, error) { + rows, err := q.db.QueryContext(ctx, findSOSPostConditions, arg.SosPostID, arg.IncludeDeleted) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindSOSPostConditionsRow + for rows.Next() { + var i FindSOSPostConditionsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &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 +} diff --git a/internal/postgres/condition_store.go b/internal/postgres/condition_store.go deleted file mode 100644 index 55c0ca74..00000000 --- a/internal/postgres/condition_store.go +++ /dev/null @@ -1,67 +0,0 @@ -package postgres - -import ( - "context" - - pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/domain/sospost" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" -) - -func InitConditions(ctx context.Context, tx *database.Tx, conditions []sospost.SOSCondition) (string, *pnd.AppError) { - const sql = ` - INSERT INTO sos_conditions - ( - id, - name, - created_at, - updated_at - ) - SELECT $1, $2, now(), now() - WHERE NOT EXISTS ( - SELECT - 1 - FROM - sos_conditions - WHERE - name = $2::VARCHAR(50) - ); - ` - - for n, v := range conditions { - _, err := tx.ExecContext(ctx, sql, n+1, string(v)) - if err != nil { - return "", pnd.FromPostgresError(err) - } - } - - return "condition init success", nil -} - -func FindConditions(ctx context.Context, tx *database.Tx) (*sospost.ConditionList, *pnd.AppError) { - const sql = ` - SELECT - id, - name - FROM - sos_conditions - ` - - conditions := make(sospost.ConditionList, 0) - rows, err := tx.QueryContext(ctx, sql) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - for rows.Next() { - condition := sospost.Condition{} - if err := rows.Scan(&condition.ID, &condition.Name); err != nil { - return nil, pnd.FromPostgresError(err) - } - conditions = append(conditions, &condition) - } - if err := rows.Err(); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return &conditions, nil -} diff --git a/internal/postgres/sos_post_store.go b/internal/postgres/sos_post_store.go index 91391a31..4d663674 100644 --- a/internal/postgres/sos_post_store.go +++ b/internal/postgres/sos_post_store.go @@ -7,6 +7,8 @@ import ( "fmt" "time" + "github.com/pet-sitter/pets-next-door-api/internal/domain/soscondition" + "github.com/pet-sitter/pets-next-door-api/internal/domain/resourcemedia" utils "github.com/pet-sitter/pets-next-door-api/internal/common" @@ -698,7 +700,7 @@ func updateSOSPostsPets(ctx context.Context, tx *database.Tx, postID int, petIDs return nil } -func FindConditionByID(ctx context.Context, tx *database.Tx, id int) (*sospost.ConditionList, *pnd.AppError) { +func FindConditionByID(ctx context.Context, tx *database.Tx, id int) (*soscondition.ViewListForSOSPost, *pnd.AppError) { const query = ` SELECT sos_conditions.id, @@ -716,7 +718,7 @@ func FindConditionByID(ctx context.Context, tx *database.Tx, id int) (*sospost.C sos_posts_conditions.deleted_at IS NULL ` - conditions := sospost.ConditionList{} + conditions := soscondition.ViewListForSOSPost{} rows, err := tx.QueryContext(ctx, query, id) if err != nil { return nil, pnd.FromPostgresError(err) @@ -724,7 +726,7 @@ func FindConditionByID(ctx context.Context, tx *database.Tx, id int) (*sospost.C defer rows.Close() for rows.Next() { - condition := sospost.Condition{} + condition := soscondition.ViewForSOSPost{} if err := rows.Scan( &condition.ID, &condition.Name, diff --git a/internal/service/condition_service.go b/internal/service/condition_service.go deleted file mode 100644 index 12aef249..00000000 --- a/internal/service/condition_service.go +++ /dev/null @@ -1,39 +0,0 @@ -package service - -import ( - "context" - - pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/domain/sospost" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - "github.com/pet-sitter/pets-next-door-api/internal/postgres" -) - -type ConditionService struct { - conn *database.DB -} - -func NewConditionService(conn *database.DB) *ConditionService { - return &ConditionService{ - conn: conn, - } -} - -func (service *ConditionService) FindConditions(ctx context.Context) ([]sospost.ConditionView, *pnd.AppError) { - tx, err := service.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return nil, err - } - - conditions, err := postgres.FindConditions(ctx, tx) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return conditions.ToConditionViewList(), nil -} diff --git a/internal/service/sos_condition_service.go b/internal/service/sos_condition_service.go new file mode 100644 index 00000000..4fdebdd6 --- /dev/null +++ b/internal/service/sos_condition_service.go @@ -0,0 +1,59 @@ +package service + +import ( + "context" + + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + + "github.com/pet-sitter/pets-next-door-api/internal/domain/soscondition" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" + + pnd "github.com/pet-sitter/pets-next-door-api/api" + "github.com/pet-sitter/pets-next-door-api/internal/infra/database" +) + +type SOSConditionService struct { + conn *database.DB +} + +func NewSOSConditionService(conn *database.DB) *SOSConditionService { + return &SOSConditionService{ + conn: conn, + } +} + +func (service *SOSConditionService) InitConditions(ctx context.Context) (soscondition.ListView, *pnd.AppError) { + tx, err := service.conn.BeginTx(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + + conditionList := make([]databasegen.SosCondition, len(soscondition.AvailableNames)) + for idx, conditionName := range soscondition.AvailableNames { + created, err := databasegen.New(tx).CreateSOSCondition(ctx, databasegen.CreateSOSConditionParams{ + ID: int32(idx + 1), + Name: utils.StrToNullStr(conditionName.String()), + }) + if err != nil { + return nil, pnd.FromPostgresError(err) + } + + conditionList[idx] = created + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return soscondition.ToListView(conditionList), nil +} + +func (service *SOSConditionService) FindConditions(ctx context.Context) (soscondition.ListView, *pnd.AppError) { + conditionList, err := databasegen.New(service.conn).FindConditions(ctx, false) + if err != nil { + return nil, pnd.FromPostgresError(err) + } + + return soscondition.ToListViewFromRows(conditionList), nil +} diff --git a/internal/service/sos_post_service.go b/internal/service/sos_post_service.go index f8953e64..54f133c0 100644 --- a/internal/service/sos_post_service.go +++ b/internal/service/sos_post_service.go @@ -3,6 +3,8 @@ package service import ( "context" + "github.com/pet-sitter/pets-next-door-api/internal/domain/soscondition" + "github.com/pet-sitter/pets-next-door-api/internal/domain/resourcemedia" "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" @@ -58,9 +60,11 @@ func (service *SOSPostService) WriteSOSPost( return nil, pnd.FromPostgresError(err2) } - conditions, err := postgres.FindConditionByID(ctx, tx, sosPost.ID) - if err != nil { - return nil, err + conditionList, err2 := databasegen.New(tx).FindSOSPostConditions(ctx, databasegen.FindSOSPostConditionsParams{ + SosPostID: utils.IntToNullInt64(sosPost.ID), + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } petRows, err2 := databasegen.New(tx).FindPetsBySOSPostID(ctx, utils.IntToNullInt64(sosPost.ID)) @@ -79,7 +83,7 @@ func (service *SOSPostService) WriteSOSPost( return sosPost.ToWriteSOSPostView( media.ToListViewFromResourceMediaRows(mediaData), - conditions.ToConditionViewList(), + soscondition.ToListViewFromSOSPostConditions(conditionList), pet.ToDetailViewList(petRows), dates.ToSOSDateViewList(), ), nil @@ -117,7 +121,7 @@ func (service *SOSPostService) FindSOSPosts( ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), }, media.ToListViewFromViewListForSOSPost(sosPost.Media), - sosPost.Conditions.ToConditionViewList(), + soscondition.ToListViewFromViewForSOSPost(sosPost.Conditions), sosPost.Pets.ToDetailViewList(), sosPost.Dates.ToSOSDateViewList(), ) @@ -159,7 +163,7 @@ func (service *SOSPostService) FindSOSPostsByAuthorID( ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), }, media.ToListViewFromViewListForSOSPost(sosPost.Media), - sosPost.Conditions.ToConditionViewList(), + soscondition.ToListViewFromViewForSOSPost(sosPost.Conditions), sosPost.Pets.ToDetailViewList(), sosPost.Dates.ToSOSDateViewList(), ) @@ -200,7 +204,7 @@ func (service *SOSPostService) FindSOSPostByID(ctx context.Context, id int) (*so ProfileImageURL: utils.NullStrToStrPtr(author.ProfileImageUrl), }, media.ToListViewFromViewListForSOSPost(sosPost.Media), - sosPost.Conditions.ToConditionViewList(), + soscondition.ToListViewFromViewForSOSPost(sosPost.Conditions), sosPost.Pets.ToDetailViewList(), sosPost.Dates.ToSOSDateViewList(), ), nil @@ -228,9 +232,11 @@ func (service *SOSPostService) UpdateSOSPost( return nil, pnd.FromPostgresError(err2) } - conditions, err := postgres.FindConditionByID(ctx, tx, updateSOSPost.ID) - if err != nil { - return nil, err + conditionList, err2 := databasegen.New(tx).FindSOSPostConditions(ctx, databasegen.FindSOSPostConditionsParams{ + SosPostID: utils.IntToNullInt64(updateSOSPost.ID), + }) + if err2 != nil { + return nil, pnd.FromPostgresError(err2) } petRows, err2 := databasegen.New(tx).FindPetsBySOSPostID(ctx, utils.IntToNullInt64(updateSOSPost.ID)) @@ -249,7 +255,7 @@ func (service *SOSPostService) UpdateSOSPost( return updateSOSPost.ToUpdateSOSPostView( media.ToListViewFromResourceMediaRows(mediaData), - conditions.ToConditionViewList(), + soscondition.ToListViewFromSOSPostConditions(conditionList), pet.ToDetailViewList(petRows), dates.ToSOSDateViewList(), ), nil diff --git a/internal/service/tests/sos_post_service_test.go b/internal/service/tests/sos_post_service_test.go index 447b64cf..1ceaf45d 100644 --- a/internal/service/tests/sos_post_service_test.go +++ b/internal/service/tests/sos_post_service_test.go @@ -5,13 +5,13 @@ import ( "reflect" "testing" - pnd "github.com/pet-sitter/pets-next-door-api/api" + "github.com/pet-sitter/pets-next-door-api/internal/domain/soscondition" + "github.com/pet-sitter/pets-next-door-api/internal/domain/media" "github.com/pet-sitter/pets-next-door-api/internal/domain/pet" "github.com/pet-sitter/pets-next-door-api/internal/domain/sospost" "github.com/pet-sitter/pets-next-door-api/internal/domain/user" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - "github.com/pet-sitter/pets-next-door-api/internal/postgres" "github.com/pet-sitter/pets-next-door-api/internal/service" "github.com/pet-sitter/pets-next-door-api/internal/tests" ) @@ -23,11 +23,9 @@ func TestSOSPostService(t *testing.T) { db, _ := database.Open(tests.TestDatabaseURL) db.Flush() - if err := database.WithTransaction(ctx, db, func(tx *database.Tx) *pnd.AppError { - postgres.InitConditions(ctx, tx, sospost.ConditionName) - return nil - }); err != nil { - t.Errorf("InitConditions failed: %v", err) + conditionService := service.NewSOSConditionService(db) + if _, err2 := conditionService.InitConditions(ctx); err2 != nil { + t.Errorf("InitConditions failed: %v", err2) } return db, func(t *testing.T) { @@ -461,11 +459,11 @@ func assertFindSOSPostEquals(t *testing.T, got sospost.FindSOSPostView, want sos } } -func assertConditionEquals(t *testing.T, got []sospost.ConditionView, want []int) { +func assertConditionEquals(t *testing.T, got soscondition.ListView, want []int) { t.Helper() for i := range want { - if i+1 != got[i].ID { + if int64(i+1) != got[i].ID { t.Errorf("got %v want %v", got[i].ID, i+1) } } diff --git a/queries/sos_conditions.sql b/queries/sos_conditions.sql new file mode 100644 index 00000000..a79b76b5 --- /dev/null +++ b/queries/sos_conditions.sql @@ -0,0 +1,35 @@ +-- name: CreateSOSCondition :one +INSERT INTO sos_conditions +(id, + name, + created_at, + updated_at) +SELECT $1, $2, now(), now() +WHERE NOT EXISTS (SELECT 1 + FROM sos_conditions + WHERE name = $2::VARCHAR(50)) +RETURNING *; + +-- name: FindConditions :many +SELECT id, + name, + created_at, + updated_at, + deleted_at +FROM sos_conditions +WHERE (sqlc.arg('include_deleted')::BOOLEAN = TRUE OR + (sqlc.arg('include_deleted')::BOOLEAN = FALSE AND deleted_at IS NULL)); + +-- name: FindSOSPostConditions :many +SELECT sos_conditions.id, + sos_conditions.name, + sos_conditions.created_at, + sos_conditions.updated_at +FROM sos_conditions + INNER JOIN + sos_posts_conditions + ON + sos_conditions.id = sos_posts_conditions.sos_condition_id +WHERE sos_posts_conditions.sos_post_id = $1 + AND (sqlc.arg('include_deleted')::BOOLEAN = TRUE OR + (sqlc.arg('include_deleted')::BOOLEAN = FALSE AND sos_posts_conditions.deleted_at IS NULL));