From fbba2c035f990ddf12079a39f63ea62895b893d9 Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Sat, 18 Jan 2025 18:05:52 +0300 Subject: [PATCH] refactor(db): move users and persons functions into separate files --- internal/db/db.go | 270 ----------------------------------------- internal/db/persons.go | 258 +++++++++++++++++++++++++++++++++++++++ internal/db/users.go | 16 +++ 3 files changed, 274 insertions(+), 270 deletions(-) create mode 100644 internal/db/persons.go create mode 100644 internal/db/users.go diff --git a/internal/db/db.go b/internal/db/db.go index 1b3fc66..63d9291 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -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 diff --git a/internal/db/persons.go b/internal/db/persons.go new file mode 100644 index 0000000..de05d51 --- /dev/null +++ b/internal/db/persons.go @@ -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 +} diff --git a/internal/db/users.go b/internal/db/users.go new file mode 100644 index 0000000..1b92114 --- /dev/null +++ b/internal/db/users.go @@ -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 +}