Add database query status (and improved response), photos, likes, comments, bans

This commit is contained in:
Marco Realacci 2022-11-20 19:21:26 +01:00
parent 519ae22197
commit abbd5bc494
22 changed files with 1118 additions and 72 deletions

View file

@ -40,25 +40,42 @@ import (
// AppDatabase is the high level interface for the DB
type AppDatabase interface {
CreateUser(name string) (string, error)
UserExists(uid string) (bool, error)
GetUserID(name string) (string, error)
UpdateUsername(uid, name string) error
CreateUser(name string) (string, error)
GetUserFollowers(uid string) ([]structures.UIDName, error)
FollowUser(uid string, follow string) error
UnfollowUser(uid string, unfollow string) error
BanUser(uid string, ban string) error
UnbanUser(uid string, unban string) error
PostPhoto(uid string) (int64, error)
LikePhoto(uid string, photo int64) error
UnlikePhoto(uid string, photo int64) error
UpdateUsername(uid, name string) error
GetUserFollowers(uid string) (QueryResult, *[]structures.UIDName, error) // todo: maybe use a pointer to a slice?
GetUserFollowing(uid string) (QueryResult, *[]structures.UIDName, error)
FollowUser(uid string, follow string) (QueryResult, error)
UnfollowUser(uid string, unfollow string) (QueryResult, error)
BanUser(uid string, ban string) (QueryResult, error)
UnbanUser(uid string, unban string) (QueryResult, error)
PostPhoto(uid string) (DBTransaction, int64, error)
DeletePhoto(uid string, photo int64) (bool, error)
GetPhotoLikes(uid string, photo int64) (QueryResult, *[]structures.UIDName, error)
LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error)
UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error)
GetUserProfile(uid string) (*UserProfile, error)
GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error)
PostComment(uid string, photo_id int64, comment_user string, comment string) (QueryResult, error)
DeleteComment(uid string, photo_id int64, comment_id int64) (QueryResult, error)
GetCommentOwner(uid string, photo_id int64, comment_id int64) (QueryResult, string, error)
Ping() error
}
type DBTransaction interface {
Commit() error
Rollback() error
}
type appdbimpl struct {
c *sql.DB
}

View file

@ -0,0 +1,112 @@
package database
import (
"time"
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
"github.com/notherealmarco/WASAPhoto/service/structures"
)
func (db *appdbimpl) PostComment(uid string, photo_id int64, comment_user string, comment string) (QueryResult, error) {
// Check if the photo exists, as API specification requires
// photos to be identified also by the user who posted them.
// But our DB implementation only requires the photo id.
exists, err := db.photoExists(uid, photo_id)
if err != nil || !exists {
return ERR_NOT_FOUND, err
}
_, err = db.c.Exec(`PRAGMA foreign_keys = ON;
INSERT INTO "comments" ("user", "photo", "comment", "date") VALUES (?, ?, ?, ?)`, comment_user, photo_id, comment, time.Now().Format(time.RFC3339))
// todo: we don't actually need it, it's already done before
if db_errors.ForeignKeyViolation(err) {
return ERR_NOT_FOUND, nil
}
if err != nil {
return ERR_INTERNAL, err
}
return SUCCESS, nil
}
func (db *appdbimpl) GetCommentOwner(uid string, photo_id int64, comment_id int64) (QueryResult, string, error) {
// Check if the photo exists, as it exist but have no comments
exists, err := db.photoExists(uid, photo_id)
if err != nil || !exists {
return ERR_NOT_FOUND, "", err
}
var comment_user string
err = db.c.QueryRow(`SELECT "user" FROM "comments" WHERE "photo" = ? AND "id" = ?`, photo_id, comment_id).Scan(&comment_user)
if db_errors.EmptySet(err) {
return ERR_NOT_FOUND, "", nil
}
if err != nil {
return ERR_INTERNAL, "", err
}
return SUCCESS, comment_user, nil
}
func (db *appdbimpl) DeleteComment(uid string, photo_id int64, comment_id int64) (QueryResult, error) {
// Check if the photo exists, as API specification requires
// photos to be identified also by the user who posted them.
// But our DB implementation only requires the photo id.
exists, err := db.photoExists(uid, photo_id)
if err != nil || !exists {
return ERR_NOT_FOUND, err
}
res, err := db.c.Exec(`DELETE FROM "comments" WHERE "photo" = ? AND "id" = ?`, photo_id, comment_id)
if err != nil {
return ERR_INTERNAL, err
}
rows, err := res.RowsAffected()
if err != nil {
return ERR_INTERNAL, err
}
if rows == 0 {
return ERR_NOT_FOUND, nil
}
return SUCCESS, nil
}
func (db *appdbimpl) GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error) {
// Check if the photo exists, as it exist but have no comments
exists, err := db.photoExists(uid, photo_id)
if err != nil || !exists {
return ERR_NOT_FOUND, nil, err
}
rows, err := db.c.Query(`SELECT "id", "user", "comment", "date" FROM "comments" WHERE "photo" = ?`, photo_id)
if err != nil {
return ERR_INTERNAL, nil, err
}
defer rows.Close()
comments := make([]structures.Comment, 0)
for rows.Next() {
var c structures.Comment
err = rows.Scan(&c.CommentID, &c.UID, &c.Comment, &c.Date)
if err != nil {
return ERR_INTERNAL, nil, err
}
comments = append(comments, c)
}
return SUCCESS, &comments, nil
}

View file

@ -1,6 +1,12 @@
package database
import "time"
import (
"database/sql"
"time"
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
"github.com/notherealmarco/WASAPhoto/service/structures"
)
type Photo struct {
ID int64
@ -16,17 +22,63 @@ type UserProfile struct {
Photos []Photo
}
type QueryResult int // todo: move to a separate file
const (
SUCCESS = 0
ERR_NOT_FOUND = 1
ERR_EXISTS = 2
ERR_INTERNAL = 3
)
type dbtransaction struct {
c *sql.Tx
}
func (tx *dbtransaction) Commit() error {
return tx.c.Commit()
}
func (tx *dbtransaction) Rollback() error {
return tx.c.Rollback()
}
// Post a new photo
func (db *appdbimpl) PostPhoto(uid string) (int64, error) {
res, err := db.c.Exec(`INSERT INTO "photos" ("user", "date") VALUES (?, ?)`, uid, time.Now().Format(time.RFC3339))
func (db *appdbimpl) PostPhoto(uid string) (DBTransaction, int64, error) {
tx, err := db.c.Begin()
if err != nil {
return 0, err
return nil, 0, err
}
res, err := tx.Exec(`INSERT INTO "photos" ("user", "date") VALUES (?, ?)`, uid, time.Now().Format(time.RFC3339))
if err != nil {
tx.Rollback() // error ?
return nil, 0, err
}
id, err := res.LastInsertId()
if err != nil {
return 0, err
tx.Rollback() // error ?
return nil, 0, err
}
return id, nil
return &dbtransaction{
c: tx,
}, id, nil
}
// Delete a photo, returns true if the photo was deleted and false if it did not exist
func (db *appdbimpl) DeletePhoto(uid string, photo int64) (bool, error) {
res, err := db.c.Exec(`DELETE FROM "photos" WHERE "id" = ? AND "user" = ?`, photo, uid)
if err != nil {
return false, err
}
rows, err := res.RowsAffected()
if err != nil {
return false, err
}
return rows > 0, nil
}
// Get user profile, including username, followers, following, and photos
@ -58,7 +110,7 @@ func (db *appdbimpl) GetUserProfile(uid string) (*UserProfile, error) {
return nil, err
}
var photos []Photo
photos := make([]Photo, 0)
for rows.Next() {
var id int64
var date string
@ -75,14 +127,105 @@ func (db *appdbimpl) GetUserProfile(uid string) (*UserProfile, error) {
return &UserProfile{uid, name, followers, following, photos}, nil
}
// Check if a given photo owned by a given user exists
func (db *appdbimpl) photoExists(uid string, photo int64) (bool, error) {
var cnt int64
err := db.c.QueryRow(`SELECT COUNT(*) FROM "photos" WHERE "id" = ? AND "user" = ?`, photo, uid).Scan(&cnt)
if err != nil {
return false, err
}
return cnt > 0, nil
}
// Get the list of users who liked a photo
func (db *appdbimpl) GetPhotoLikes(uid string, photo int64) (QueryResult, *[]structures.UIDName, error) {
// Check if the photo exists, as it could exist but have no likes
exists, err := db.photoExists(uid, photo)
if err != nil {
return ERR_INTERNAL, nil, err
}
if !exists {
return ERR_NOT_FOUND, nil, nil
}
rows, err := db.c.Query(`SELECT "users.uid", "users.name" FROM "likes", "users"
WHERE "likes.photo_id" = ?
AND "likes.user" = "users.uid"`, photo)
if err != nil {
return ERR_INTERNAL, nil, err
}
likes := make([]structures.UIDName, 0)
for rows.Next() {
var uid string
var name string
err = rows.Scan(&uid, &name)
if err != nil {
return ERR_INTERNAL, nil, err
}
likes = append(likes, structures.UIDName{UID: uid, Name: name})
}
return SUCCESS, &likes, nil
}
// Like a photo
func (db *appdbimpl) LikePhoto(uid string, photo int64) error {
_, err := db.c.Exec(`INSERT INTO "likes" ("user", "photo_id") VALUES (?, ?)`, uid, photo)
return err
func (db *appdbimpl) LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) {
// Check if the photo exists, as API specification requires
// photos to be identified also by the user who posted them.
// But our DB implementation only requires the photo id.
exists, err := db.photoExists(uid, photo)
if err != nil || !exists {
return ERR_NOT_FOUND, err
}
_, err = db.c.Exec(`PRAGMA foreign_keys = ON;
INSERT INTO "likes" ("user", "photo_id") VALUES (?, ?)`, liker_uid, photo)
// The photo exists, but the user already liked it
if db_errors.UniqueViolation(err) {
return ERR_EXISTS, nil
}
if db_errors.ForeignKeyViolation(err) {
return ERR_NOT_FOUND, nil
}
if err != nil {
return ERR_INTERNAL, err
}
return SUCCESS, nil
}
// Unlike a photo
func (db *appdbimpl) UnlikePhoto(uid string, photo int64) error {
_, err := db.c.Exec(`DELETE FROM "likes" WHERE "user" = ? AND "photo_id" = ?`, uid, photo)
return err
func (db *appdbimpl) UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) {
// Check if the photo exists, as API specification requires
// photos to be identified also by the user who posted them.
// But our DB implementation only requires the photo id.
exists, err := db.photoExists(uid, photo)
if err != nil || !exists {
return ERR_NOT_FOUND, err
}
res, err := db.c.Exec(`DELETE FROM "likes" WHERE "user" = ? AND "photo_id" = ?`, liker_uid, photo)
if err != nil {
return ERR_INTERNAL, err
}
rows, err := res.RowsAffected()
if err != nil {
return ERR_INTERNAL, err
}
if rows == 0 {
return ERR_NOT_FOUND, nil
}
return SUCCESS, nil
}

View file

@ -1,6 +1,8 @@
package database
import (
"database/sql"
"github.com/gofrs/uuid"
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
"github.com/notherealmarco/WASAPhoto/service/structures"
@ -46,10 +48,62 @@ func (db *appdbimpl) UpdateUsername(uid string, name string) error {
}
// Get user followers
func (db *appdbimpl) GetUserFollowers(uid string) ([]structures.UIDName, error) {
func (db *appdbimpl) GetUserFollowers(uid string) (QueryResult, *[]structures.UIDName, error) {
// user may exist but have no followers
exists, err := db.UserExists(uid)
if err != nil {
return ERR_INTERNAL, nil, err
}
if !exists {
return ERR_NOT_FOUND, nil, nil
}
rows, err := db.c.Query(`SELECT "follower", "user.name" FROM "follows", "users"
WHERE "follows.follower" = "users.uid"
AND "followed" = ?`, uid)
followers, err := db.uidNameQuery(rows, err)
if err != nil {
return ERR_INTERNAL, nil, err
}
return SUCCESS, followers, nil
}
// Get user following
func (db *appdbimpl) GetUserFollowing(uid string) (QueryResult, *[]structures.UIDName, error) {
// user may exist but have no followers
exists, err := db.UserExists(uid)
if err != nil {
return ERR_INTERNAL, nil, err
}
if !exists {
return ERR_NOT_FOUND, nil, nil
}
rows, err := db.c.Query(`SELECT "followed", "user.name" FROM "follows", "users"
WHERE "follows.followed" = "users.uid"
AND "follower" = ?`, uid)
following, err := db.uidNameQuery(rows, err)
if err != nil {
return ERR_INTERNAL, nil, err
}
return SUCCESS, following, nil
}
// Evaluates a query that returns two columns: uid and name
func (db *appdbimpl) uidNameQuery(rows *sql.Rows, err error) (*[]structures.UIDName, error) {
if err != nil {
return nil, err
}
@ -64,29 +118,88 @@ func (db *appdbimpl) GetUserFollowers(uid string) ([]structures.UIDName, error)
}
followers = append(followers, structures.UIDName{UID: uid, Name: name})
}
return followers, nil
return &followers, nil
}
// Follow a user
func (db *appdbimpl) FollowUser(uid string, follow string) error {
_, err := db.c.Exec(`INSERT INTO "follows" ("follower", "followed") VALUES (?, ?)`, uid, follow)
return err
func (db *appdbimpl) FollowUser(uid string, follow string) (QueryResult, error) {
_, err := db.c.Exec(`PRAGMA foreign_keys = ON;
INSERT INTO "follows" ("follower", "followed") VALUES (?, ?)`, uid, follow)
if db_errors.UniqueViolation(err) {
return ERR_EXISTS, nil
}
if db_errors.ForeignKeyViolation(err) {
return ERR_NOT_FOUND, nil
}
if err != nil {
return ERR_INTERNAL, err
}
return SUCCESS, nil
}
// Unfollow a user
func (db *appdbimpl) UnfollowUser(uid string, unfollow string) error {
_, err := db.c.Exec(`DELETE FROM "follows" WHERE "follower" = ? AND "followed" = ?`, uid, unfollow)
return err
} //todo: should return boolean or something similar
func (db *appdbimpl) UnfollowUser(uid string, unfollow string) (QueryResult, error) {
res, err := db.c.Exec(`DELETE FROM "follows" WHERE "follower" = ? AND "followed" = ?`, uid, unfollow)
if err != nil {
return ERR_INTERNAL, err
}
rows, err := res.RowsAffected()
if err != nil {
return ERR_INTERNAL, err
}
if rows == 0 {
return ERR_NOT_FOUND, nil
}
return SUCCESS, nil
}
// Ban a user
func (db *appdbimpl) BanUser(uid string, ban string) error {
_, err := db.c.Exec(`INSERT INTO "bans" ("user", "ban") VALUES (?, ?)`, uid, ban)
return err
func (db *appdbimpl) BanUser(uid string, ban string) (QueryResult, error) {
_, err := db.c.Exec(`PRAGMA foreign_keys = ON;
INSERT INTO "bans" ("user", "ban") VALUES (?, ?)`, uid, ban)
// The user is already banned by this user
if db_errors.UniqueViolation(err) {
return ERR_EXISTS, nil
}
// One of the users does not exist
if db_errors.ForeignKeyViolation(err) {
return ERR_NOT_FOUND, nil
}
// Other error
if err != nil {
return ERR_INTERNAL, err
}
return SUCCESS, nil
}
// Unban a user
func (db *appdbimpl) UnbanUser(uid string, unban string) error {
_, err := db.c.Exec(`DELETE FROM "bans" WHERE "user" = ? AND "ban" = ?`, uid, unban)
return err
func (db *appdbimpl) UnbanUser(uid string, unban string) (QueryResult, error) {
res, err := db.c.Exec(`DELETE FROM "bans" WHERE "user" = ? AND "ban" = ?`, uid, unban)
if err != nil {
return ERR_INTERNAL, err
}
rows, err := res.RowsAffected()
if err != nil {
return ERR_INTERNAL, err
}
// The user was not banned by this user
if rows == 0 {
return ERR_NOT_FOUND, nil
}
return SUCCESS, nil
}

View file

@ -9,3 +9,17 @@ func EmptySet(err error) bool {
}
return strings.Contains(err.Error(), "no rows in result set")
}
func UniqueViolation(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "UNIQUE constraint failed")
}
func ForeignKeyViolation(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "FOREIGN KEY constraint failed")
}