added ratings and updated ebook struct
This commit is contained in:
@@ -83,6 +83,13 @@ The application uses Go HTML templates for server-side rendering with HTMX for d
|
|||||||
- `GET /api/ebooks/:id/progress` - Get reading progress
|
- `GET /api/ebooks/:id/progress` - Get reading progress
|
||||||
- `PUT /api/ebooks/:id/progress` - Update reading progress
|
- `PUT /api/ebooks/:id/progress` - Update reading progress
|
||||||
|
|
||||||
|
### Ratings (Protected)
|
||||||
|
- `GET /api/ebooks/:id/rating` - Get user's rating for ebook
|
||||||
|
- `POST /api/ebooks/:id/rating` - Create or update ebook rating
|
||||||
|
- `PUT /api/ebooks/:id/rating` - Create or update ebook rating
|
||||||
|
- `DELETE /api/ebooks/:id/rating` - Delete user's rating
|
||||||
|
- `GET /api/ebooks/:id/ratings` - Get all ratings for ebook
|
||||||
|
|
||||||
## API Testing
|
## API Testing
|
||||||
|
|
||||||
Use the included Bruno collection in the `bruno/` directory for testing the API:
|
Use the included Bruno collection in the `bruno/` directory for testing the API:
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type EbookRatings struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
|
||||||
|
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Rating int32 `db:"rating" json:"rating"`
|
||||||
|
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type Ebooks struct {
|
type Ebooks struct {
|
||||||
ID pgtype.UUID `db:"id" json:"id"`
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
Title string `db:"title" json:"title"`
|
Title string `db:"title" json:"title"`
|
||||||
@@ -20,6 +29,13 @@ type Ebooks struct {
|
|||||||
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
||||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||||
|
Series pgtype.Text `db:"series" json:"series"`
|
||||||
|
SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"`
|
||||||
|
Tags pgtype.Text `db:"tags" json:"tags"`
|
||||||
|
Asin pgtype.Text `db:"asin" json:"asin"`
|
||||||
|
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
|
||||||
|
Publisher pgtype.Text `db:"publisher" json:"publisher"`
|
||||||
|
Contributors pgtype.Text `db:"contributors" json:"contributors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReadingProgress struct {
|
type ReadingProgress struct {
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ import (
|
|||||||
|
|
||||||
type Querier interface {
|
type Querier interface {
|
||||||
CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error)
|
CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error)
|
||||||
|
CreateEbookRating(ctx context.Context, arg CreateEbookRatingParams) (EbookRatings, error)
|
||||||
CreateUser(ctx context.Context, arg CreateUserParams) (Users, error)
|
CreateUser(ctx context.Context, arg CreateUserParams) (Users, error)
|
||||||
DeleteEbook(ctx context.Context, id pgtype.UUID) error
|
DeleteEbook(ctx context.Context, id pgtype.UUID) error
|
||||||
|
DeleteEbookRating(ctx context.Context, arg DeleteEbookRatingParams) error
|
||||||
DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error
|
DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error
|
||||||
GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error)
|
GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error)
|
||||||
|
GetEbookRating(ctx context.Context, arg GetEbookRatingParams) (EbookRatings, error)
|
||||||
|
GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]GetEbookRatingsRow, error)
|
||||||
GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error)
|
GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error)
|
||||||
GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error)
|
GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error)
|
||||||
GetUserByEmail(ctx context.Context, email string) (Users, error)
|
GetUserByEmail(ctx context.Context, email string) (Users, error)
|
||||||
@@ -24,6 +28,7 @@ type Querier interface {
|
|||||||
ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebooks, error)
|
ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebooks, error)
|
||||||
ListUsers(ctx context.Context) ([]ListUsersRow, error)
|
ListUsers(ctx context.Context) ([]ListUsersRow, error)
|
||||||
UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error)
|
UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error)
|
||||||
|
UpdateEbookRating(ctx context.Context, arg UpdateEbookRatingParams) (EbookRatings, error)
|
||||||
UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error)
|
UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error)
|
||||||
UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error
|
UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const CreateEbook = `-- name: CreateEbook :one
|
const CreateEbook = `-- name: CreateEbook :one
|
||||||
INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path)
|
INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at
|
RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at, series, series_number, tags, asin, date_published, publisher, contributors
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateEbookParams struct {
|
type CreateEbookParams struct {
|
||||||
@@ -26,6 +26,13 @@ type CreateEbookParams struct {
|
|||||||
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
|
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
|
||||||
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
|
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
|
||||||
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
||||||
|
Series pgtype.Text `db:"series" json:"series"`
|
||||||
|
SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"`
|
||||||
|
Tags pgtype.Text `db:"tags" json:"tags"`
|
||||||
|
Asin pgtype.Text `db:"asin" json:"asin"`
|
||||||
|
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
|
||||||
|
Publisher pgtype.Text `db:"publisher" json:"publisher"`
|
||||||
|
Contributors pgtype.Text `db:"contributors" json:"contributors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error) {
|
func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error) {
|
||||||
@@ -38,6 +45,13 @@ func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebook
|
|||||||
arg.FileSize,
|
arg.FileSize,
|
||||||
arg.MimeType,
|
arg.MimeType,
|
||||||
arg.CoverImagePath,
|
arg.CoverImagePath,
|
||||||
|
arg.Series,
|
||||||
|
arg.SeriesNumber,
|
||||||
|
arg.Tags,
|
||||||
|
arg.Asin,
|
||||||
|
arg.DatePublished,
|
||||||
|
arg.Publisher,
|
||||||
|
arg.Contributors,
|
||||||
)
|
)
|
||||||
var i Ebooks
|
var i Ebooks
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -52,6 +66,43 @@ func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebook
|
|||||||
&i.CoverImagePath,
|
&i.CoverImagePath,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Series,
|
||||||
|
&i.SeriesNumber,
|
||||||
|
&i.Tags,
|
||||||
|
&i.Asin,
|
||||||
|
&i.DatePublished,
|
||||||
|
&i.Publisher,
|
||||||
|
&i.Contributors,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateEbookRating = `-- name: CreateEbookRating :one
|
||||||
|
INSERT INTO ebook_ratings (ebook_id, user_id, rating)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (ebook_id, user_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
rating = EXCLUDED.rating,
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING id, ebook_id, user_id, rating, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateEbookRatingParams struct {
|
||||||
|
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
|
||||||
|
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Rating int32 `db:"rating" json:"rating"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateEbookRating(ctx context.Context, arg CreateEbookRatingParams) (EbookRatings, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateEbookRating, arg.EbookID, arg.UserID, arg.Rating)
|
||||||
|
var i EbookRatings
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.EbookID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Rating,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -98,6 +149,20 @@ func (q *Queries) DeleteEbook(ctx context.Context, id pgtype.UUID) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DeleteEbookRating = `-- name: DeleteEbookRating :exec
|
||||||
|
DELETE FROM ebook_ratings WHERE ebook_id = $1 AND user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteEbookRatingParams struct {
|
||||||
|
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
|
||||||
|
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) DeleteEbookRating(ctx context.Context, arg DeleteEbookRatingParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteEbookRating, arg.EbookID, arg.UserID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const DeleteReadingProgress = `-- name: DeleteReadingProgress :exec
|
const DeleteReadingProgress = `-- name: DeleteReadingProgress :exec
|
||||||
DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2
|
DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2
|
||||||
`
|
`
|
||||||
@@ -113,7 +178,7 @@ func (q *Queries) DeleteReadingProgress(ctx context.Context, arg DeleteReadingPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetEbook = `-- name: GetEbook :one
|
const GetEbook = `-- name: GetEbook :one
|
||||||
SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at FROM ebooks WHERE id = $1
|
SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at, series, series_number, tags, asin, date_published, publisher, contributors FROM ebooks WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error) {
|
func (q *Queries) GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error) {
|
||||||
@@ -131,10 +196,86 @@ func (q *Queries) GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error)
|
|||||||
&i.CoverImagePath,
|
&i.CoverImagePath,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Series,
|
||||||
|
&i.SeriesNumber,
|
||||||
|
&i.Tags,
|
||||||
|
&i.Asin,
|
||||||
|
&i.DatePublished,
|
||||||
|
&i.Publisher,
|
||||||
|
&i.Contributors,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetEbookRating = `-- name: GetEbookRating :one
|
||||||
|
SELECT id, ebook_id, user_id, rating, created_at, updated_at FROM ebook_ratings WHERE ebook_id = $1 AND user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetEbookRatingParams struct {
|
||||||
|
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
|
||||||
|
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetEbookRating(ctx context.Context, arg GetEbookRatingParams) (EbookRatings, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetEbookRating, arg.EbookID, arg.UserID)
|
||||||
|
var i EbookRatings
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.EbookID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Rating,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetEbookRatings = `-- name: GetEbookRatings :many
|
||||||
|
SELECT er.id, er.ebook_id, er.user_id, er.rating, er.created_at, er.updated_at, u.username
|
||||||
|
FROM ebook_ratings er
|
||||||
|
JOIN users u ON er.user_id = u.id
|
||||||
|
WHERE er.ebook_id = $1
|
||||||
|
ORDER BY er.created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetEbookRatingsRow struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
|
||||||
|
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Rating int32 `db:"rating" json:"rating"`
|
||||||
|
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||||
|
Username string `db:"username" json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]GetEbookRatingsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetEbookRatings, ebookID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []GetEbookRatingsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetEbookRatingsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.EbookID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Rating,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.Username,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const GetReadingProgress = `-- name: GetReadingProgress :one
|
const GetReadingProgress = `-- name: GetReadingProgress :one
|
||||||
SELECT id, ebook_id, user_id, current_page, total_pages, last_read_at FROM reading_progress WHERE ebook_id = $1 AND user_id = $2
|
SELECT id, ebook_id, user_id, current_page, total_pages, last_read_at FROM reading_progress WHERE ebook_id = $1 AND user_id = $2
|
||||||
`
|
`
|
||||||
@@ -243,7 +384,7 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListEbooks = `-- name: ListEbooks :many
|
const ListEbooks = `-- name: ListEbooks :many
|
||||||
SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2
|
SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at, series, series_number, tags, asin, date_published, publisher, contributors FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListEbooksParams struct {
|
type ListEbooksParams struct {
|
||||||
@@ -272,6 +413,13 @@ func (q *Queries) ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebook
|
|||||||
&i.CoverImagePath,
|
&i.CoverImagePath,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Series,
|
||||||
|
&i.SeriesNumber,
|
||||||
|
&i.Tags,
|
||||||
|
&i.Asin,
|
||||||
|
&i.DatePublished,
|
||||||
|
&i.Publisher,
|
||||||
|
&i.Contributors,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -330,9 +478,16 @@ UPDATE ebooks SET
|
|||||||
isbn = $4,
|
isbn = $4,
|
||||||
description = $5,
|
description = $5,
|
||||||
cover_image_path = $6,
|
cover_image_path = $6,
|
||||||
|
series = $7,
|
||||||
|
series_number = $8,
|
||||||
|
tags = $9,
|
||||||
|
asin = $10,
|
||||||
|
date_published = $11,
|
||||||
|
publisher = $12,
|
||||||
|
contributors = $13,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at
|
RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at, series, series_number, tags, asin, date_published, publisher, contributors
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateEbookParams struct {
|
type UpdateEbookParams struct {
|
||||||
@@ -342,6 +497,13 @@ type UpdateEbookParams struct {
|
|||||||
Isbn pgtype.Text `db:"isbn" json:"isbn"`
|
Isbn pgtype.Text `db:"isbn" json:"isbn"`
|
||||||
Description pgtype.Text `db:"description" json:"description"`
|
Description pgtype.Text `db:"description" json:"description"`
|
||||||
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
||||||
|
Series pgtype.Text `db:"series" json:"series"`
|
||||||
|
SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"`
|
||||||
|
Tags pgtype.Text `db:"tags" json:"tags"`
|
||||||
|
Asin pgtype.Text `db:"asin" json:"asin"`
|
||||||
|
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
|
||||||
|
Publisher pgtype.Text `db:"publisher" json:"publisher"`
|
||||||
|
Contributors pgtype.Text `db:"contributors" json:"contributors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error) {
|
func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error) {
|
||||||
@@ -352,6 +514,13 @@ func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebook
|
|||||||
arg.Isbn,
|
arg.Isbn,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
arg.CoverImagePath,
|
arg.CoverImagePath,
|
||||||
|
arg.Series,
|
||||||
|
arg.SeriesNumber,
|
||||||
|
arg.Tags,
|
||||||
|
arg.Asin,
|
||||||
|
arg.DatePublished,
|
||||||
|
arg.Publisher,
|
||||||
|
arg.Contributors,
|
||||||
)
|
)
|
||||||
var i Ebooks
|
var i Ebooks
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -366,6 +535,41 @@ func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebook
|
|||||||
&i.CoverImagePath,
|
&i.CoverImagePath,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.Series,
|
||||||
|
&i.SeriesNumber,
|
||||||
|
&i.Tags,
|
||||||
|
&i.Asin,
|
||||||
|
&i.DatePublished,
|
||||||
|
&i.Publisher,
|
||||||
|
&i.Contributors,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateEbookRating = `-- name: UpdateEbookRating :one
|
||||||
|
UPDATE ebook_ratings SET
|
||||||
|
rating = $3,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE ebook_id = $1 AND user_id = $2
|
||||||
|
RETURNING id, ebook_id, user_id, rating, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateEbookRatingParams struct {
|
||||||
|
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
|
||||||
|
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Rating int32 `db:"rating" json:"rating"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateEbookRating(ctx context.Context, arg UpdateEbookRatingParams) (EbookRatings, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateEbookRating, arg.EbookID, arg.UserID, arg.Rating)
|
||||||
|
var i EbookRatings
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.EbookID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Rating,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ SELECT * FROM ebooks WHERE id = $1;
|
|||||||
SELECT * FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2;
|
SELECT * FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2;
|
||||||
|
|
||||||
-- name: CreateEbook :one
|
-- name: CreateEbook :one
|
||||||
INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path)
|
INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: UpdateEbook :one
|
-- name: UpdateEbook :one
|
||||||
@@ -36,6 +36,13 @@ UPDATE ebooks SET
|
|||||||
isbn = $4,
|
isbn = $4,
|
||||||
description = $5,
|
description = $5,
|
||||||
cover_image_path = $6,
|
cover_image_path = $6,
|
||||||
|
series = $7,
|
||||||
|
series_number = $8,
|
||||||
|
tags = $9,
|
||||||
|
asin = $10,
|
||||||
|
date_published = $11,
|
||||||
|
publisher = $12,
|
||||||
|
contributors = $13,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
@@ -60,4 +67,33 @@ RETURNING *;
|
|||||||
DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2;
|
DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2;
|
||||||
|
|
||||||
-- name: UpdateUserTheme :exec
|
-- name: UpdateUserTheme :exec
|
||||||
UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1;
|
UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: CreateEbookRating :one
|
||||||
|
INSERT INTO ebook_ratings (ebook_id, user_id, rating)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (ebook_id, user_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
rating = EXCLUDED.rating,
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetEbookRating :one
|
||||||
|
SELECT * FROM ebook_ratings WHERE ebook_id = $1 AND user_id = $2;
|
||||||
|
|
||||||
|
-- name: GetEbookRatings :many
|
||||||
|
SELECT er.*, u.username
|
||||||
|
FROM ebook_ratings er
|
||||||
|
JOIN users u ON er.user_id = u.id
|
||||||
|
WHERE er.ebook_id = $1
|
||||||
|
ORDER BY er.created_at DESC;
|
||||||
|
|
||||||
|
-- name: UpdateEbookRating :one
|
||||||
|
UPDATE ebook_ratings SET
|
||||||
|
rating = $3,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE ebook_id = $1 AND user_id = $2
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteEbookRating :exec
|
||||||
|
DELETE FROM ebook_ratings WHERE ebook_id = $1 AND user_id = $2;
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bookmann/internal/database"
|
"bookmann/internal/database"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
@@ -20,6 +21,17 @@ func NewHandler(db *database.Queries) *Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseDate parses a date string in YYYY-MM-DD format
|
||||||
|
func parseDate(dateStr string) time.Time {
|
||||||
|
if dateStr == "" {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
if t, err := time.Parse("2006-01-02", dateStr); err == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
func SetupRoutes(g *echo.Group, db *database.Queries) {
|
func SetupRoutes(g *echo.Group, db *database.Queries) {
|
||||||
h := NewHandler(db)
|
h := NewHandler(db)
|
||||||
|
|
||||||
@@ -31,6 +43,12 @@ func SetupRoutes(g *echo.Group, db *database.Queries) {
|
|||||||
|
|
||||||
g.GET("/ebooks/:id/progress", h.GetReadingProgress)
|
g.GET("/ebooks/:id/progress", h.GetReadingProgress)
|
||||||
g.PUT("/ebooks/:id/progress", h.UpdateReadingProgress)
|
g.PUT("/ebooks/:id/progress", h.UpdateReadingProgress)
|
||||||
|
|
||||||
|
g.GET("/ebooks/:id/rating", h.GetEbookRating)
|
||||||
|
g.POST("/ebooks/:id/rating", h.CreateOrUpdateEbookRating)
|
||||||
|
g.PUT("/ebooks/:id/rating", h.CreateOrUpdateEbookRating)
|
||||||
|
g.DELETE("/ebooks/:id/rating", h.DeleteEbookRating)
|
||||||
|
g.GET("/ebooks/:id/ratings", h.GetEbookRatings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListEbooks handles GET /api/ebooks
|
// ListEbooks handles GET /api/ebooks
|
||||||
@@ -89,6 +107,13 @@ type CreateEbookRequest struct {
|
|||||||
FileSize int64 `json:"file_size" validate:"required,min=1"`
|
FileSize int64 `json:"file_size" validate:"required,min=1"`
|
||||||
MimeType string `json:"mime_type" validate:"required"`
|
MimeType string `json:"mime_type" validate:"required"`
|
||||||
CoverImagePath string `json:"cover_image_path"`
|
CoverImagePath string `json:"cover_image_path"`
|
||||||
|
Series string `json:"series"`
|
||||||
|
SeriesNumber int32 `json:"series_number"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
ASIN string `json:"asin"`
|
||||||
|
DatePublished string `json:"date_published"`
|
||||||
|
Publisher string `json:"publisher"`
|
||||||
|
Contributors string `json:"contributors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateEbook handles POST /api/ebooks
|
// CreateEbook handles POST /api/ebooks
|
||||||
@@ -111,6 +136,13 @@ func (h *Handler) CreateEbook(c echo.Context) error {
|
|||||||
FileSize: pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0},
|
FileSize: pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0},
|
||||||
MimeType: pgtype.Text{String: req.MimeType, Valid: req.MimeType != ""},
|
MimeType: pgtype.Text{String: req.MimeType, Valid: req.MimeType != ""},
|
||||||
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
||||||
|
Series: pgtype.Text{String: req.Series, Valid: req.Series != ""},
|
||||||
|
SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0},
|
||||||
|
Tags: pgtype.Text{String: req.Tags, Valid: req.Tags != ""},
|
||||||
|
Asin: pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""},
|
||||||
|
DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
|
||||||
|
Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
|
||||||
|
Contributors: pgtype.Text{String: req.Contributors, Valid: req.Contributors != ""},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
@@ -126,6 +158,13 @@ type UpdateEbookRequest struct {
|
|||||||
ISBN string `json:"isbn"`
|
ISBN string `json:"isbn"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
CoverImagePath string `json:"cover_image_path"`
|
CoverImagePath string `json:"cover_image_path"`
|
||||||
|
Series string `json:"series"`
|
||||||
|
SeriesNumber int32 `json:"series_number"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
ASIN string `json:"asin"`
|
||||||
|
DatePublished string `json:"date_published"`
|
||||||
|
Publisher string `json:"publisher"`
|
||||||
|
Contributors string `json:"contributors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateEbook handles PUT /api/ebooks/:id
|
// UpdateEbook handles PUT /api/ebooks/:id
|
||||||
@@ -152,6 +191,13 @@ func (h *Handler) UpdateEbook(c echo.Context) error {
|
|||||||
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
|
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
|
||||||
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
||||||
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
||||||
|
Series: pgtype.Text{String: req.Series, Valid: req.Series != ""},
|
||||||
|
SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0},
|
||||||
|
Tags: pgtype.Text{String: req.Tags, Valid: req.Tags != ""},
|
||||||
|
Asin: pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""},
|
||||||
|
DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
|
||||||
|
Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
|
||||||
|
Contributors: pgtype.Text{String: req.Contributors, Valid: req.Contributors != ""},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
@@ -250,3 +296,114 @@ func (h *Handler) UpdateReadingProgress(c echo.Context) error {
|
|||||||
|
|
||||||
return c.JSON(http.StatusOK, progress)
|
return c.JSON(http.StatusOK, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdateEbookRatingRequest represents the request for creating/updating an ebook rating
|
||||||
|
type CreateOrUpdateEbookRatingRequest struct {
|
||||||
|
Rating int32 `json:"rating" validate:"required,min=1,max=5"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEbookRating handles GET /api/ebooks/:id/rating
|
||||||
|
func (h *Handler) GetEbookRating(c echo.Context) error {
|
||||||
|
ebookIdStr := c.Param("id")
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
|
||||||
|
ebookId, err := uuid.Parse(ebookIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
rating, err := h.db.GetEbookRating(c.Request().Context(), database.GetEbookRatingParams{
|
||||||
|
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// If no rating found, return 404
|
||||||
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "rating not found"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdateEbookRating handles POST/PUT /api/ebooks/:id/rating
|
||||||
|
func (h *Handler) CreateOrUpdateEbookRating(c echo.Context) error {
|
||||||
|
ebookIdStr := c.Param("id")
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
|
||||||
|
ebookId, err := uuid.Parse(ebookIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateOrUpdateEbookRatingRequest
|
||||||
|
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||||
|
}
|
||||||
|
if err := c.Validate(&req); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
rating, err := h.db.CreateEbookRating(c.Request().Context(), database.CreateEbookRatingParams{
|
||||||
|
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Rating: req.Rating,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEbookRating handles DELETE /api/ebooks/:id/rating
|
||||||
|
func (h *Handler) DeleteEbookRating(c echo.Context) error {
|
||||||
|
ebookIdStr := c.Param("id")
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
|
||||||
|
ebookId, err := uuid.Parse(ebookIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.db.DeleteEbookRating(c.Request().Context(), database.DeleteEbookRatingParams{
|
||||||
|
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEbookRatings handles GET /api/ebooks/:id/ratings
|
||||||
|
func (h *Handler) GetEbookRatings(c echo.Context) error {
|
||||||
|
ebookIdStr := c.Param("id")
|
||||||
|
|
||||||
|
ebookId, err := uuid.Parse(ebookIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
|
||||||
|
}
|
||||||
|
|
||||||
|
ratings, err := h.db.GetEbookRatings(c.Request().Context(), pgtype.UUID{Bytes: ebookId, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, ratings)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- Add ebook ratings functionality
|
||||||
|
CREATE TABLE ebook_ratings (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
ebook_id UUID NOT NULL REFERENCES ebooks(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
UNIQUE(ebook_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes for better query performance
|
||||||
|
CREATE INDEX idx_ebook_ratings_ebook_id ON ebook_ratings(ebook_id);
|
||||||
|
CREATE INDEX idx_ebook_ratings_user_id ON ebook_ratings(user_id);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Add additional metadata fields to ebooks table
|
||||||
|
ALTER TABLE ebooks ADD COLUMN series VARCHAR(255);
|
||||||
|
ALTER TABLE ebooks ADD COLUMN series_number INTEGER;
|
||||||
|
ALTER TABLE ebooks ADD COLUMN tags TEXT;
|
||||||
|
ALTER TABLE ebooks ADD COLUMN asin VARCHAR(20);
|
||||||
|
ALTER TABLE ebooks ADD COLUMN date_published DATE;
|
||||||
|
ALTER TABLE ebooks ADD COLUMN publisher VARCHAR(255);
|
||||||
|
ALTER TABLE ebooks ADD COLUMN contributors TEXT;
|
||||||
@@ -28,6 +28,12 @@ This directory contains Bruno collection for testing the Bookmann API with compr
|
|||||||
- **Get Reading Progress**: GET /api/ebooks/:id/progress - User's progress
|
- **Get Reading Progress**: GET /api/ebooks/:id/progress - User's progress
|
||||||
- **Update Reading Progress**: PUT /api/ebooks/:id/progress - Update progress
|
- **Update Reading Progress**: PUT /api/ebooks/:id/progress - Update progress
|
||||||
|
|
||||||
|
### Ratings (Protected - JWT Required)
|
||||||
|
- **Get Ebook Rating**: GET /api/ebooks/:id/rating - User's rating for ebook
|
||||||
|
- **Create/Update Rating**: POST /api/ebooks/:id/rating - Rate ebook (1-5 stars)
|
||||||
|
- **Delete Rating**: DELETE /api/ebooks/:id/rating - Remove user's rating
|
||||||
|
- **Get All Ratings**: GET /api/ebooks/:id/ratings - All ratings for ebook
|
||||||
|
|
||||||
## Documentation Features
|
## Documentation Features
|
||||||
|
|
||||||
Each request includes:
|
Each request includes:
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ body:json {
|
|||||||
"file_path": "/uploads/sample.epub",
|
"file_path": "/uploads/sample.epub",
|
||||||
"file_size": 1024000,
|
"file_size": 1024000,
|
||||||
"mime_type": "application/epub+zip",
|
"mime_type": "application/epub+zip",
|
||||||
"cover_image_path": "/uploads/cover.jpg"
|
"cover_image_path": "/uploads/cover.jpg",
|
||||||
|
"series": "Sample Series",
|
||||||
|
"series_number": 1,
|
||||||
|
"tags": "fiction,adventure",
|
||||||
|
"asin": "B0123456789",
|
||||||
|
"date_published": "2023-01-15",
|
||||||
|
"publisher": "Sample Publisher",
|
||||||
|
"contributors": "Editor Name, Illustrator Name"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,15 +50,22 @@ docs {
|
|||||||
|
|
||||||
**Authentication:** Required (Bearer token)
|
**Authentication:** Required (Bearer token)
|
||||||
|
|
||||||
**Request Body:**
|
**Request Body:**
|
||||||
- `title` (string, required): Book title
|
- `title` (string, required): Book title
|
||||||
- `author` (string, optional): Book author
|
- `author` (string, optional): Book author
|
||||||
- `isbn` (string, optional): ISBN number
|
- `isbn` (string, optional): ISBN number
|
||||||
- `description` (string, optional): Book description
|
- `description` (string, optional): Book description
|
||||||
- `file_path` (string, required): Path to ebook file
|
- `file_path` (string, required): Path to ebook file
|
||||||
- `file_size` (number, optional): File size in bytes
|
- `file_size` (number, optional): File size in bytes
|
||||||
- `mime_type` (string, optional): MIME type
|
- `mime_type` (string, optional): MIME type
|
||||||
- `cover_image_path` (string, optional): Path to cover image
|
- `cover_image_path` (string, optional): Path to cover image
|
||||||
|
- `series` (string, optional): Book series name
|
||||||
|
- `series_number` (number, optional): Position in series
|
||||||
|
- `tags` (string, optional): Comma-separated tags
|
||||||
|
- `asin` (string, optional): Amazon ASIN
|
||||||
|
- `date_published` (string, optional): Publication date (YYYY-MM-DD)
|
||||||
|
- `publisher` (string, optional): Publisher name
|
||||||
|
- `contributors` (string, optional): Additional contributors
|
||||||
|
|
||||||
**Response:** Created ebook object
|
**Response:** Created ebook object
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
meta {
|
||||||
|
name: Create/Update Ebook Rating
|
||||||
|
type: http
|
||||||
|
seq: 7
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: http://localhost:8765/api/ebooks/{{ebookid}}/rating
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body: json {
|
||||||
|
{
|
||||||
|
"rating": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docs {
|
||||||
|
## Create/Update Ebook Rating
|
||||||
|
|
||||||
|
Creates or updates the authenticated user's rating for a specific ebook.
|
||||||
|
|
||||||
|
**Authentication:** Required (Bearer token)
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `id` (string): Ebook UUID
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
- `rating` (number, required): Rating value (1-5)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- `id` (string): Rating UUID
|
||||||
|
- `ebook_id` (string): Ebook UUID
|
||||||
|
- `user_id` (string): User UUID
|
||||||
|
- `rating` (number): Rating value (1-5)
|
||||||
|
- `created_at` (string): Creation timestamp
|
||||||
|
- `updated_at` (string): Last update timestamp
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- 400: Invalid rating value
|
||||||
|
- 401: Invalid authentication
|
||||||
|
- 404: Ebook not found
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
meta {
|
||||||
|
name: Delete Ebook Rating
|
||||||
|
type: http
|
||||||
|
seq: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
delete {
|
||||||
|
url: http://localhost:8765/api/ebooks/{{ebookid}}/rating
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docs {
|
||||||
|
## Delete Ebook Rating
|
||||||
|
|
||||||
|
Deletes the authenticated user's rating for a specific ebook.
|
||||||
|
|
||||||
|
**Authentication:** Required (Bearer token)
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `id` (string): Ebook UUID
|
||||||
|
|
||||||
|
**Response:** No content (204)
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- 401: Invalid authentication
|
||||||
|
- 404: Rating not found
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
meta {
|
||||||
|
name: Get Ebook Rating
|
||||||
|
type: http
|
||||||
|
seq: 6
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: http://localhost:8765/api/ebooks/{{ebookid}}/rating
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docs {
|
||||||
|
## Get Ebook Rating
|
||||||
|
|
||||||
|
Retrieves the authenticated user's rating for a specific ebook.
|
||||||
|
|
||||||
|
**Authentication:** Required (Bearer token)
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `id` (string): Ebook UUID
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- `id` (string): Rating UUID
|
||||||
|
- `ebook_id` (string): Ebook UUID
|
||||||
|
- `user_id` (string): User UUID
|
||||||
|
- `rating` (number): Rating value (1-5)
|
||||||
|
- `created_at` (string): Creation timestamp
|
||||||
|
- `updated_at` (string): Last update timestamp
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- 401: Invalid authentication
|
||||||
|
- 404: Rating not found
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
meta {
|
||||||
|
name: Get Ebook Ratings
|
||||||
|
type: http
|
||||||
|
seq: 9
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: http://localhost:8765/api/ebooks/{{ebookid}}/ratings
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docs {
|
||||||
|
## Get Ebook Ratings
|
||||||
|
|
||||||
|
Retrieves all ratings for a specific ebook, including usernames.
|
||||||
|
|
||||||
|
**Authentication:** Required (Bearer token)
|
||||||
|
|
||||||
|
**Path Parameters:**
|
||||||
|
- `id` (string): Ebook UUID
|
||||||
|
|
||||||
|
**Response:** Array of rating objects
|
||||||
|
- `id` (string): Rating UUID
|
||||||
|
- `ebook_id` (string): Ebook UUID
|
||||||
|
- `user_id` (string): User UUID
|
||||||
|
- `rating` (number): Rating value (1-5)
|
||||||
|
- `created_at` (string): Creation timestamp
|
||||||
|
- `updated_at` (string): Last update timestamp
|
||||||
|
- `username` (string): Username of the rating user
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
- 401: Invalid authentication
|
||||||
|
- 404: Ebook not found
|
||||||
|
}
|
||||||
@@ -14,7 +14,14 @@ body:json {
|
|||||||
{
|
{
|
||||||
"title": "Updated Book Title",
|
"title": "Updated Book Title",
|
||||||
"author": "Updated Author",
|
"author": "Updated Author",
|
||||||
"description": "Updated description"
|
"description": "Updated description",
|
||||||
|
"series": "Updated Series",
|
||||||
|
"series_number": 2,
|
||||||
|
"tags": "fiction,drama",
|
||||||
|
"asin": "B0987654321",
|
||||||
|
"date_published": "2023-06-15",
|
||||||
|
"publisher": "Updated Publisher",
|
||||||
|
"contributors": "Updated Editor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,12 +40,19 @@ docs {
|
|||||||
**Path Parameters:**
|
**Path Parameters:**
|
||||||
- `id` (string): Ebook UUID
|
- `id` (string): Ebook UUID
|
||||||
|
|
||||||
**Request Body:** (all fields optional)
|
**Request Body:** (all fields optional)
|
||||||
- `title` (string): Book title
|
- `title` (string): Book title
|
||||||
- `author` (string): Book author
|
- `author` (string): Book author
|
||||||
- `isbn` (string): ISBN number
|
- `isbn` (string): ISBN number
|
||||||
- `description` (string): Book description
|
- `description` (string): Book description
|
||||||
- `cover_image_path` (string): Path to cover image
|
- `cover_image_path` (string): Path to cover image
|
||||||
|
- `series` (string): Book series name
|
||||||
|
- `series_number` (number): Position in series
|
||||||
|
- `tags` (string): Comma-separated tags
|
||||||
|
- `asin` (string): Amazon ASIN
|
||||||
|
- `date_published` (string): Publication date (YYYY-MM-DD)
|
||||||
|
- `publisher` (string): Publisher name
|
||||||
|
- `contributors` (string): Additional contributors
|
||||||
|
|
||||||
**Response:** Updated ebook object
|
**Response:** Updated ebook object
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user