refactor(backend): change backend file structure
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"tanabata/internal/domain"
|
||||
)
|
||||
|
||||
// Check if user can view file
|
||||
func FileGetAccess(user_id int, file_id string) (canView, canEdit bool, err error) {
|
||||
ctx := context.Background()
|
||||
row := connPool.QueryRow(ctx, `
|
||||
WITH extras AS (
|
||||
SELECT
|
||||
(SELECT creator_id FROM data.files WHERE id=$2)=$1 AS is_creator,
|
||||
(SELECT is_admin FROM system.users WHERE id=$1) AS is_admin
|
||||
)
|
||||
SELECT
|
||||
af.view OR (SELECT is_creator OR is_admin FROM extras),
|
||||
af.edit OR (SELECT is_creator OR is_admin FROM extras)
|
||||
FROM acl.files af
|
||||
WHERE af.user_id=$1 AND af.file_id=$2
|
||||
`, user_id, file_id)
|
||||
err = row.Scan(&canView, &canEdit)
|
||||
if err == pgx.ErrNoRows {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get a set of files
|
||||
func FileGetSlice(user_id int, filter, sort string, limit, offset int) (files domain.Slice[domain.FileItem], statusCode int, err error) {
|
||||
filterCond, statusCode, err := filterToSQL(filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sortExpr, statusCode, err := sortToSQL(sort)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// prepare query
|
||||
query := `
|
||||
SELECT
|
||||
f.id,
|
||||
f.name,
|
||||
m.name,
|
||||
m.extension,
|
||||
uuid_extract_timestamp(f.id),
|
||||
u.name,
|
||||
u.is_admin
|
||||
FROM data.files f
|
||||
JOIN system.mime m ON m.id=f.mime_id
|
||||
JOIN system.users u ON u.id=f.creator_id
|
||||
WHERE NOT f.is_deleted AND (f.creator_id=$1 OR (SELECT view FROM acl.files WHERE file_id=f.id AND user_id=$1) OR (SELECT is_admin FROM system.users WHERE id=$1)) AND
|
||||
`
|
||||
query += filterCond
|
||||
queryCount := query
|
||||
query += sortExpr
|
||||
if limit >= 0 {
|
||||
query += fmt.Sprintf(" LIMIT %d", limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
query += fmt.Sprintf(" OFFSET %d", offset)
|
||||
}
|
||||
// execute query
|
||||
statusCode, err = transaction(func(ctx context.Context, tx pgx.Tx) (statusCode int, err error) {
|
||||
rows, err := tx.Query(ctx, query, user_id)
|
||||
if err != nil {
|
||||
statusCode, err = handleDBError(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var file domain.FileItem
|
||||
err = rows.Scan(&file.ID, &file.Name, &file.MIME.Name, &file.MIME.Extension, &file.CreatedAt, &file.Creator.Name, &file.Creator.IsAdmin)
|
||||
if err != nil {
|
||||
statusCode = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
files.Data = append(files.Data, file)
|
||||
count++
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
statusCode = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
files.Pagination.Limit = limit
|
||||
files.Pagination.Offset = offset
|
||||
files.Pagination.Count = count
|
||||
row := tx.QueryRow(ctx, fmt.Sprintf("SELECT COUNT(*) FROM (%s) tmp", queryCount), user_id)
|
||||
err = row.Scan(&files.Pagination.Total)
|
||||
if err != nil {
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
return
|
||||
})
|
||||
if err == nil {
|
||||
statusCode = http.StatusOK
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get file
|
||||
func FileGet(user_id int, file_id string) (file domain.FileFull, statusCode int, err error) {
|
||||
ctx := context.Background()
|
||||
row := connPool.QueryRow(ctx, `
|
||||
SELECT
|
||||
f.id,
|
||||
f.name,
|
||||
m.name,
|
||||
m.extension,
|
||||
uuid_extract_timestamp(f.id),
|
||||
u.name,
|
||||
u.is_admin,
|
||||
f.notes,
|
||||
f.metadata,
|
||||
(SELECT COUNT(*) FROM activity.file_views fv WHERE fv.file_id=$2 AND fv.user_id=$1)
|
||||
FROM data.files f
|
||||
JOIN system.mime m ON m.id=f.mime_id
|
||||
JOIN system.users u ON u.id=f.creator_id
|
||||
WHERE NOT f.is_deleted AND f.id=$2 AND (f.creator_id=$1 OR (SELECT view FROM acl.files WHERE file_id=$2 AND user_id=$1) OR (SELECT is_admin FROM system.users WHERE id=$1))
|
||||
`, user_id, file_id)
|
||||
err = row.Scan(&file.ID, &file.Name, &file.MIME.Name, &file.MIME.Extension, &file.CreatedAt, &file.Creator.Name, &file.Creator.IsAdmin, &file.Notes, &file.Metadata, &file.Viewed)
|
||||
if err != nil {
|
||||
statusCode, err = handleDBError(err)
|
||||
return
|
||||
}
|
||||
rows, err := connPool.Query(ctx, `
|
||||
SELECT
|
||||
t.id,
|
||||
t.name,
|
||||
COALESCE(t.color, c.color)
|
||||
FROM data.tags t
|
||||
LEFT JOIN data.categories c ON c.id=t.category_id
|
||||
JOIN data.file_tag ft ON ft.tag_id=t.id
|
||||
WHERE ft.file_id=$1
|
||||
`, file_id)
|
||||
if err != nil {
|
||||
statusCode, err = handleDBError(err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var tag domain.TagCore
|
||||
err = rows.Scan(&tag.ID, &tag.Name, &tag.Color)
|
||||
if err != nil {
|
||||
statusCode = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
file.Tags = append(file.Tags, tag)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
statusCode = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
statusCode = http.StatusOK
|
||||
return
|
||||
}
|
||||
|
||||
// Add file
|
||||
func FileAdd(user_id int, name, mime string, datetime time.Time, notes string, metadata json.RawMessage) (file domain.FileCore, statusCode int, err error) {
|
||||
ctx := context.Background()
|
||||
var mime_id int
|
||||
var extension string
|
||||
row := connPool.QueryRow(ctx, "SELECT id, extension FROM system.mime WHERE name=$1", mime)
|
||||
err = row.Scan(&mime_id, &extension)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
err = fmt.Errorf("unsupported file type: %q", mime)
|
||||
statusCode = http.StatusBadRequest
|
||||
} else {
|
||||
statusCode, err = handleDBError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
row = connPool.QueryRow(ctx, `
|
||||
INSERT INTO data.files (name, mime_id, datetime, creator_id, notes, metadata)
|
||||
VALUES (NULLIF($1, ''), $2, $3, $4, NULLIF($5 ,''), $6)
|
||||
RETURNING id
|
||||
`, name, mime_id, datetime, user_id, notes, metadata)
|
||||
err = row.Scan(&file.ID)
|
||||
if err != nil {
|
||||
statusCode, err = handleDBError(err)
|
||||
return
|
||||
}
|
||||
file.Name.String = name
|
||||
file.Name.Valid = (name != "")
|
||||
file.MIME.Name = mime
|
||||
file.MIME.Extension = extension
|
||||
statusCode = http.StatusOK
|
||||
return
|
||||
}
|
||||
|
||||
// Update file
|
||||
func FileUpdate(user_id int, file_id string, updates map[string]interface{}) (statusCode int, err error) {
|
||||
if len(updates) == 0 {
|
||||
err = fmt.Errorf("no fields provided for update")
|
||||
statusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
writableFields := map[string]bool{
|
||||
"name": true,
|
||||
"datetime": true,
|
||||
"notes": true,
|
||||
"metadata": true,
|
||||
}
|
||||
query := "UPDATE data.files SET"
|
||||
newValues := []interface{}{user_id}
|
||||
count := 2
|
||||
for field, value := range updates {
|
||||
if !writableFields[field] {
|
||||
err = fmt.Errorf("invalid field: %q", field)
|
||||
statusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
query += fmt.Sprintf(" %s=NULLIF($%d, '')", field, count)
|
||||
newValues = append(newValues, value)
|
||||
count++
|
||||
}
|
||||
query += fmt.Sprintf(
|
||||
" WHERE id=$%d AND (creator_id=$1 OR (SELECT edit FROM acl.files WHERE file_id=$%d AND user_id=$1) OR (SELECT is_admin FROM system.users WHERE id=$1))",
|
||||
count, count)
|
||||
newValues = append(newValues, file_id)
|
||||
ctx := context.Background()
|
||||
commandTag, err := connPool.Exec(ctx, query, newValues...)
|
||||
if err != nil {
|
||||
statusCode, err = handleDBError(err)
|
||||
return
|
||||
}
|
||||
if commandTag.RowsAffected() == 0 {
|
||||
err = fmt.Errorf("not found")
|
||||
statusCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
statusCode = http.StatusNoContent
|
||||
return
|
||||
}
|
||||
|
||||
// Delete file
|
||||
func FileDelete(user_id int, file_id string) (statusCode int, err error) {
|
||||
ctx := context.Background()
|
||||
commandTag, err := connPool.Exec(ctx,
|
||||
"DELETE FROM data.files WHERE id=$2 AND (creator_id=$1 OR (SELECT edit FROM acl.files WHERE file_id=$2 AND user_id=$1) OR (SELECT is_admin FROM system.users WHERE id=$1))",
|
||||
user_id, file_id)
|
||||
if err != nil {
|
||||
statusCode, err = handleDBError(err)
|
||||
return
|
||||
}
|
||||
if commandTag.RowsAffected() == 0 {
|
||||
err = fmt.Errorf("not found")
|
||||
statusCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
statusCode = http.StatusNoContent
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
var connPool *pgxpool.Pool
|
||||
|
||||
func InitDB(connString string) error {
|
||||
poolConfig, err := pgxpool.ParseConfig(connString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while parsing connection string: %w", err)
|
||||
}
|
||||
|
||||
poolConfig.MaxConns = 100
|
||||
poolConfig.MinConns = 0
|
||||
poolConfig.MaxConnLifetime = time.Hour
|
||||
poolConfig.HealthCheckPeriod = 30 * time.Second
|
||||
|
||||
connPool, err = pgxpool.NewWithConfig(context.Background(), poolConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while initializing DB connections pool: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func transaction(handler func(context.Context, pgx.Tx) (statusCode int, err error)) (statusCode int, err error) {
|
||||
ctx := context.Background()
|
||||
tx, err := connPool.Begin(ctx)
|
||||
if err != nil {
|
||||
statusCode = http.StatusInternalServerError
|
||||
return
|
||||
}
|
||||
statusCode, err = handler(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback(ctx)
|
||||
return
|
||||
}
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle database error
|
||||
func handleDBError(errIn error) (statusCode int, err error) {
|
||||
if errIn == nil {
|
||||
statusCode = http.StatusOK
|
||||
return
|
||||
}
|
||||
if errors.Is(errIn, pgx.ErrNoRows) {
|
||||
err = fmt.Errorf("not found")
|
||||
statusCode = http.StatusNotFound
|
||||
return
|
||||
}
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(errIn, &pgErr) {
|
||||
switch pgErr.Code {
|
||||
case "22P02", "22007": // Invalid data format
|
||||
err = fmt.Errorf("%s", pgErr.Message)
|
||||
statusCode = http.StatusBadRequest
|
||||
return
|
||||
case "23505": // Unique constraint violation
|
||||
err = fmt.Errorf("already exists")
|
||||
statusCode = http.StatusConflict
|
||||
return
|
||||
}
|
||||
}
|
||||
return http.StatusInternalServerError, errIn
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Convert "filter" URL param to SQL "WHERE" condition
|
||||
func filterToSQL(filter string) (sql string, statusCode int, err error) {
|
||||
// filterTokens := strings.Split(string(filter), ";")
|
||||
sql = "(true)"
|
||||
return
|
||||
}
|
||||
|
||||
// Convert "sort" URL param to SQL "ORDER BY"
|
||||
func sortToSQL(sort string) (sql string, statusCode int, err error) {
|
||||
if sort == "" {
|
||||
return
|
||||
}
|
||||
sortOptions := strings.Split(sort, ",")
|
||||
sql = " ORDER BY "
|
||||
for i, sortOption := range sortOptions {
|
||||
sortOrder := sortOption[:1]
|
||||
sortColumn := sortOption[1:]
|
||||
// parse sorting order marker
|
||||
switch sortOrder {
|
||||
case "+":
|
||||
sortOrder = "ASC"
|
||||
case "-":
|
||||
sortOrder = "DESC"
|
||||
default:
|
||||
err = fmt.Errorf("invalid sorting order mark: %q", sortOrder)
|
||||
statusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
// validate sorting column
|
||||
var n int
|
||||
n, err = strconv.Atoi(sortColumn)
|
||||
if err != nil || n < 0 {
|
||||
err = fmt.Errorf("invalid sorting column: %q", sortColumn)
|
||||
statusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
// add sorting option to query
|
||||
if i > 0 {
|
||||
sql += ","
|
||||
}
|
||||
sql += fmt.Sprintf("%s %s NULLS LAST", sortColumn, sortOrder)
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user