refactor(db): move users and persons functions into separate files

This commit is contained in:
Masahiko AMANO 2025-01-18 18:05:52 +03:00
parent 171ca5e52c
commit fbba2c035f
3 changed files with 274 additions and 270 deletions

View File

@ -4,12 +4,9 @@ import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/H1K0/Kiraku/internal/models"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
@ -50,270 +47,3 @@ func transaction(ctx context.Context, handler func(pgx.Tx) (statusCode int, err
}
return
}
//#region Users
func UserLogin(ctx context.Context, login, password string) (user_id string, err error) {
row := connPool.QueryRow(ctx, "SELECT id FROM users WHERE login=$1 AND password=crypt($2, password)", login, password)
err = row.Scan(&user_id)
return
}
func UserAuth(ctx context.Context, user_id string) (ok, editor bool) {
row := connPool.QueryRow(ctx, "SELECT editor FROM users WHERE id=$1", user_id)
err := row.Scan(&editor)
ok = (err == nil)
return
}
//#endregion Users
//#region Persons
func PersonsGet(ctx context.Context, user_id, filter, sort string, limit, offset int) (persons models.Persons, statusCode int, err error) {
ok, _ := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
queryGet := "SELECT id, name, COALESCE(sort_name, '') FROM persons WHERE POSITION($1 IN LOWER(name))>0 OR POSITION($1 IN LOWER(sort_name))>0"
if sort != "" {
sort_options := strings.Split(sort, ",")
queryGet += " ORDER BY "
for i, sort_option := range sort_options {
sort_order := sort_option[:1]
sort_field := sort_option[1:]
switch sort_order {
case "+":
sort_order = "ASC"
case "-":
sort_order = "DESC"
default:
err = fmt.Errorf("invalid sorting order mark: %q", sort)
statusCode = http.StatusBadRequest
return
}
switch sort_field {
case "name":
case "sortName":
sort_field = "COALESCE(sort_name, name)"
case "birthdate":
case "deathdate":
default:
err = fmt.Errorf("invalid sorting field: %q", sort_field)
statusCode = http.StatusBadRequest
return
}
if i > 0 {
queryGet += ", "
}
queryGet += fmt.Sprintf("%s %s NULLS LAST", sort_field, sort_order)
}
}
queryCount := queryGet
if limit >= 0 {
queryGet += fmt.Sprintf(" LIMIT %d", limit)
}
if offset > 0 {
queryGet += fmt.Sprintf(" OFFSET %d", offset)
}
filter = strings.ToLower(filter)
statusCode, err = transaction(ctx, func(tx pgx.Tx) (statusCode int, err error) {
rows, err := tx.Query(ctx, queryGet, filter)
if err != nil {
statusCode = http.StatusInternalServerError
return
}
count := 0
for rows.Next() {
var person models.PersonBrief
err = rows.Scan(&person.ID, &person.Name, &person.SortName)
if err != nil {
statusCode = http.StatusInternalServerError
return
}
persons.Persons = append(persons.Persons, person)
count++
}
err = rows.Err()
if err != nil {
statusCode = http.StatusInternalServerError
return
}
persons.Pagination.Limit = limit
persons.Pagination.Offset = offset
persons.Pagination.Count = count
queryCount = fmt.Sprintf("SELECT COUNT(*) FROM (%s) tmp", queryCount)
row := tx.QueryRow(ctx, queryCount, filter)
err = row.Scan(&persons.Pagination.Total)
if err != nil {
statusCode = http.StatusInternalServerError
}
return
})
if err != nil {
return
}
statusCode = http.StatusOK
return
}
func PersonGet(ctx context.Context, user_id, person_id string) (person models.Person, statusCode int, err error) {
ok, _ := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
row := connPool.QueryRow(ctx, "SELECT id, name, COALESCE(sort_name, ''), COALESCE(TO_CHAR(birthdate, 'YYYY-MM-DD'), ''), COALESCE(TO_CHAR(deathdate, 'YYYY-MM-DD'), ''), COALESCE(info, '') FROM persons WHERE id=$1", person_id)
err = row.Scan(&person.ID, &person.Name, &person.SortName, &person.Birthdate, &person.Deathdate, &person.Deathdate)
if err != nil {
pgErr := err.(*pgconn.PgError)
if err == pgx.ErrNoRows {
err = fmt.Errorf("not found")
statusCode = http.StatusNotFound
} else if pgErr.Code == "22P02" || pgErr.Code == "22007" {
err = fmt.Errorf("%s", pgErr.Message)
statusCode = http.StatusBadRequest
} else {
statusCode = http.StatusInternalServerError
}
return
}
statusCode = http.StatusOK
return
}
func PersonAdd(ctx context.Context, user_id, name, sortName, birthdate, deathdate, info string) (person models.Person, statusCode int, err error) {
ok, editor := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
if !editor {
err = fmt.Errorf("not allowed")
statusCode = http.StatusForbidden
return
}
row := connPool.QueryRow(
ctx,
"INSERT INTO persons (name, sort_name, birthdate, deathdate, info) "+
"VALUES ($1, NULLIF($2, ''), NULLIF($3, '')::date, NULLIF($4, '')::date, NULLIF($5, '')) "+
"RETURNING id, name, COALESCE(sort_name, ''), COALESCE(TO_CHAR(birthdate, 'YYYY-MM-DD'), ''), COALESCE(TO_CHAR(deathdate, 'YYYY-MM-DD'), ''), COALESCE(info, '')",
name, sortName, birthdate, deathdate, info,
)
err = row.Scan(&person.ID, &person.Name, &person.SortName, &person.Birthdate, &person.Deathdate, &person.Info)
if err != nil {
pgErr := err.(*pgconn.PgError)
if pgErr.Code == "22P02" || pgErr.Code == "22007" {
err = fmt.Errorf("%s", pgErr.Message)
statusCode = http.StatusBadRequest
} else if pgErr.Code == "23505" {
err = fmt.Errorf("a person with this name already exists")
statusCode = http.StatusConflict
} else {
statusCode = http.StatusInternalServerError
}
return
}
statusCode = http.StatusOK
return
}
func PersonUpdate(ctx context.Context, user_id, person_id string, values map[string]string) (person models.Person, statusCode int, err error) {
ok, editor := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
if !editor {
err = fmt.Errorf("not allowed")
statusCode = http.StatusForbidden
return
}
statusCode, err = transaction(ctx, func(tx pgx.Tx) (statusCode int, err error) {
for _, field := range []string{"name", "sortName", "birthdate", "deathdate", "info"} {
value, ok := values[field]
if !ok {
continue
}
if field == "sortName" {
field = "sort_name"
}
var query string
if field == "birthdate" || field == "deathdate" {
query = fmt.Sprintf("UPDATE persons SET %s=NULLIF($2, '')::date WHERE id=$1", field)
} else {
query = fmt.Sprintf("UPDATE persons SET %s=NULLIF($2, '') WHERE id=$1", field)
}
var commandTag pgconn.CommandTag
commandTag, err = tx.Exec(ctx, query, person_id, value)
if err != nil {
pgErr := err.(*pgconn.PgError)
if pgErr.Code == "22P02" || pgErr.Code == "22007" {
err = fmt.Errorf("%s", pgErr.Message)
statusCode = http.StatusBadRequest
} else if pgErr.Code == "23505" {
err = fmt.Errorf("a person with this name already exists")
statusCode = http.StatusConflict
} else {
statusCode = http.StatusInternalServerError
}
return
}
if commandTag.RowsAffected() == 0 {
err = fmt.Errorf("not found")
statusCode = http.StatusNotFound
return
}
}
row := tx.QueryRow(ctx, "SELECT id, name, COALESCE(sort_name, ''), COALESCE(TO_CHAR(birthdate, 'YYYY-MM-DD'), ''), COALESCE(TO_CHAR(deathdate, 'YYYY-MM-DD'), ''), COALESCE(info, '') FROM persons WHERE id=$1", person_id)
err = row.Scan(&person.ID, &person.Name, &person.SortName, &person.Birthdate, &person.Deathdate, &person.Info)
if err != nil {
statusCode = http.StatusInternalServerError
}
return
})
if err != nil {
return
}
statusCode = http.StatusOK
return
}
func PersonDelete(ctx context.Context, user_id, person_id string) (statusCode int, err error) {
ok, editor := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
if !editor {
err = fmt.Errorf("not allowed")
statusCode = http.StatusForbidden
return
}
commandTag, err := connPool.Exec(ctx, "DELETE FROM persons WHERE id=$1", person_id)
if err != nil {
pgErr := err.(*pgconn.PgError)
if pgErr.Code == "22P02" {
err = fmt.Errorf("%s", pgErr.Message)
statusCode = http.StatusBadRequest
} else {
statusCode = http.StatusInternalServerError
}
return
}
if commandTag.RowsAffected() == 0 {
err = fmt.Errorf("not found")
statusCode = http.StatusNotFound
return
}
statusCode = http.StatusNoContent
return
}
//#endregion Persons

258
internal/db/persons.go Normal file
View File

@ -0,0 +1,258 @@
package db
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/H1K0/Kiraku/internal/models"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
func PersonsGet(ctx context.Context, user_id, filter, sort string, limit, offset int) (persons models.Persons, statusCode int, err error) {
ok, _ := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
queryGet := "SELECT id, name, COALESCE(sort_name, '') FROM persons WHERE POSITION($1 IN LOWER(name))>0 OR POSITION($1 IN LOWER(sort_name))>0"
if sort != "" {
sort_options := strings.Split(sort, ",")
queryGet += " ORDER BY "
for i, sort_option := range sort_options {
sort_order := sort_option[:1]
sort_field := sort_option[1:]
switch sort_order {
case "+":
sort_order = "ASC"
case "-":
sort_order = "DESC"
default:
err = fmt.Errorf("invalid sorting order mark: %q", sort)
statusCode = http.StatusBadRequest
return
}
switch sort_field {
case "name":
case "sortName":
sort_field = "COALESCE(sort_name, name)"
case "birthdate":
case "deathdate":
default:
err = fmt.Errorf("invalid sorting field: %q", sort_field)
statusCode = http.StatusBadRequest
return
}
if i > 0 {
queryGet += ", "
}
queryGet += fmt.Sprintf("%s %s NULLS LAST", sort_field, sort_order)
}
}
queryCount := queryGet
if limit >= 0 {
queryGet += fmt.Sprintf(" LIMIT %d", limit)
}
if offset > 0 {
queryGet += fmt.Sprintf(" OFFSET %d", offset)
}
filter = strings.ToLower(filter)
statusCode, err = transaction(ctx, func(tx pgx.Tx) (statusCode int, err error) {
rows, err := tx.Query(ctx, queryGet, filter)
if err != nil {
statusCode = http.StatusInternalServerError
return
}
count := 0
for rows.Next() {
var person models.PersonBrief
err = rows.Scan(&person.ID, &person.Name, &person.SortName)
if err != nil {
statusCode = http.StatusInternalServerError
return
}
persons.Persons = append(persons.Persons, person)
count++
}
err = rows.Err()
if err != nil {
statusCode = http.StatusInternalServerError
return
}
persons.Pagination.Limit = limit
persons.Pagination.Offset = offset
persons.Pagination.Count = count
queryCount = fmt.Sprintf("SELECT COUNT(*) FROM (%s) tmp", queryCount)
row := tx.QueryRow(ctx, queryCount, filter)
err = row.Scan(&persons.Pagination.Total)
if err != nil {
statusCode = http.StatusInternalServerError
}
return
})
if err != nil {
return
}
statusCode = http.StatusOK
return
}
func PersonGet(ctx context.Context, user_id, person_id string) (person models.Person, statusCode int, err error) {
ok, _ := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
row := connPool.QueryRow(ctx, "SELECT id, name, COALESCE(sort_name, ''), COALESCE(TO_CHAR(birthdate, 'YYYY-MM-DD'), ''), COALESCE(TO_CHAR(deathdate, 'YYYY-MM-DD'), ''), COALESCE(info, '') FROM persons WHERE id=$1", person_id)
err = row.Scan(&person.ID, &person.Name, &person.SortName, &person.Birthdate, &person.Deathdate, &person.Deathdate)
if err != nil {
pgErr := err.(*pgconn.PgError)
if err == pgx.ErrNoRows {
err = fmt.Errorf("not found")
statusCode = http.StatusNotFound
} else if pgErr.Code == "22P02" || pgErr.Code == "22007" {
err = fmt.Errorf("%s", pgErr.Message)
statusCode = http.StatusBadRequest
} else {
statusCode = http.StatusInternalServerError
}
return
}
statusCode = http.StatusOK
return
}
func PersonAdd(ctx context.Context, user_id, name, sortName, birthdate, deathdate, info string) (person models.Person, statusCode int, err error) {
ok, editor := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
if !editor {
err = fmt.Errorf("not allowed")
statusCode = http.StatusForbidden
return
}
row := connPool.QueryRow(
ctx,
"INSERT INTO persons (name, sort_name, birthdate, deathdate, info) "+
"VALUES ($1, NULLIF($2, ''), NULLIF($3, '')::date, NULLIF($4, '')::date, NULLIF($5, '')) "+
"RETURNING id, name, COALESCE(sort_name, ''), COALESCE(TO_CHAR(birthdate, 'YYYY-MM-DD'), ''), COALESCE(TO_CHAR(deathdate, 'YYYY-MM-DD'), ''), COALESCE(info, '')",
name, sortName, birthdate, deathdate, info,
)
err = row.Scan(&person.ID, &person.Name, &person.SortName, &person.Birthdate, &person.Deathdate, &person.Info)
if err != nil {
pgErr := err.(*pgconn.PgError)
if pgErr.Code == "22P02" || pgErr.Code == "22007" {
err = fmt.Errorf("%s", pgErr.Message)
statusCode = http.StatusBadRequest
} else if pgErr.Code == "23505" {
err = fmt.Errorf("a person with this name already exists")
statusCode = http.StatusConflict
} else {
statusCode = http.StatusInternalServerError
}
return
}
statusCode = http.StatusOK
return
}
func PersonUpdate(ctx context.Context, user_id, person_id string, values map[string]string) (person models.Person, statusCode int, err error) {
ok, editor := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
if !editor {
err = fmt.Errorf("not allowed")
statusCode = http.StatusForbidden
return
}
statusCode, err = transaction(ctx, func(tx pgx.Tx) (statusCode int, err error) {
for _, field := range []string{"name", "sortName", "birthdate", "deathdate", "info"} {
value, ok := values[field]
if !ok {
continue
}
if field == "sortName" {
field = "sort_name"
}
var query string
if field == "birthdate" || field == "deathdate" {
query = fmt.Sprintf("UPDATE persons SET %s=NULLIF($2, '')::date WHERE id=$1", field)
} else {
query = fmt.Sprintf("UPDATE persons SET %s=NULLIF($2, '') WHERE id=$1", field)
}
var commandTag pgconn.CommandTag
commandTag, err = tx.Exec(ctx, query, person_id, value)
if err != nil {
pgErr := err.(*pgconn.PgError)
if pgErr.Code == "22P02" || pgErr.Code == "22007" {
err = fmt.Errorf("%s", pgErr.Message)
statusCode = http.StatusBadRequest
} else if pgErr.Code == "23505" {
err = fmt.Errorf("a person with this name already exists")
statusCode = http.StatusConflict
} else {
statusCode = http.StatusInternalServerError
}
return
}
if commandTag.RowsAffected() == 0 {
err = fmt.Errorf("not found")
statusCode = http.StatusNotFound
return
}
}
row := tx.QueryRow(ctx, "SELECT id, name, COALESCE(sort_name, ''), COALESCE(TO_CHAR(birthdate, 'YYYY-MM-DD'), ''), COALESCE(TO_CHAR(deathdate, 'YYYY-MM-DD'), ''), COALESCE(info, '') FROM persons WHERE id=$1", person_id)
err = row.Scan(&person.ID, &person.Name, &person.SortName, &person.Birthdate, &person.Deathdate, &person.Info)
if err != nil {
statusCode = http.StatusInternalServerError
}
return
})
if err != nil {
return
}
statusCode = http.StatusOK
return
}
func PersonDelete(ctx context.Context, user_id, person_id string) (statusCode int, err error) {
ok, editor := UserAuth(ctx, user_id)
if !ok {
err = fmt.Errorf("unauthorized")
statusCode = http.StatusUnauthorized
return
}
if !editor {
err = fmt.Errorf("not allowed")
statusCode = http.StatusForbidden
return
}
commandTag, err := connPool.Exec(ctx, "DELETE FROM persons WHERE id=$1", person_id)
if err != nil {
pgErr := err.(*pgconn.PgError)
if pgErr.Code == "22P02" {
err = fmt.Errorf("%s", pgErr.Message)
statusCode = http.StatusBadRequest
} else {
statusCode = http.StatusInternalServerError
}
return
}
if commandTag.RowsAffected() == 0 {
err = fmt.Errorf("not found")
statusCode = http.StatusNotFound
return
}
statusCode = http.StatusNoContent
return
}

16
internal/db/users.go Normal file
View File

@ -0,0 +1,16 @@
package db
import "context"
func UserLogin(ctx context.Context, login, password string) (user_id string, err error) {
row := connPool.QueryRow(ctx, "SELECT id FROM users WHERE login=$1 AND password=crypt($2, password)", login, password)
err = row.Scan(&user_id)
return
}
func UserAuth(ctx context.Context, user_id string) (ok, editor bool) {
row := connPool.QueryRow(ctx, "SELECT editor FROM users WHERE id=$1", user_id)
err := row.Scan(&editor)
ok = (err == nil)
return
}