feat(ebooks): add ISBN normalization and graceful library requirement handling

- Increase ISBN column from VARCHAR(13) to VARCHAR(17) to support ISBN-13 with hyphens
- Add normalize_isbn() database function to automatically remove hyphens and spaces
- Create trigger to auto-normalize ISBNs on INSERT/UPDATE operations
- Update all Ebook and MediaItem queries to use ISBN normalization
- Add GetEbookLibraryID query to check for existing ebook libraries
- Add graceful error handling when no ebook library exists
- Return helpful error message: 'no ebook library found. Please create an ebook library first'
- Create comprehensive tests for ISBN normalization and library selection
- Add Bruno test files for various ISBN formats and error scenarios
- Update documentation with ISBN normalization details
This commit is contained in:
2026-01-29 10:52:14 -05:00
parent 6ed69005b5
commit 66f1eb11a0
10 changed files with 709 additions and 24 deletions
@@ -0,0 +1,36 @@
meta {
name: Create Ebook with Various ISBN Formats
type: http
seq: 9
}
post {
url: {{base_url}}/api/ebooks
body: json
auth: inherit
}
body:json {
{
"title": "Ebook with ISBN-13 (with hyphens)",
"author": "Test Author",
"isbn": "978-0-12345-678-9",
"description": "Testing ISBN normalization with hyphens",
"file_path": "/path/to/ebook1.epub",
"file_size": 1048576,
"mime_type": "application/epub+zip"
}
}
settings {
encodeUrl: true
timeout: 0
}
docs {
## Create Ebook with ISBN-13 (with hyphens)
**Expected Behavior:** ISBN should be normalized to `9780123456789`
This test demonstrates that ISBNs with hyphens are automatically normalized.
}
@@ -0,0 +1,36 @@
meta {
name: Create Ebook with ISBN (with spaces)
type: http
seq: 10
}
post {
url: {{base_url}}/api/ebooks
body: json
auth: inherit
}
body:json {
{
"title": "Ebook with ISBN-13 (with spaces)",
"author": "Test Author",
"isbn": "978 0123456789",
"description": "Testing ISBN normalization with spaces",
"file_path": "/path/to/ebook2.epub",
"file_size": 1048576,
"mime_type": "application/epub+zip"
}
}
settings {
encodeUrl: true
timeout: 0
}
docs {
## Create Ebook with ISBN-13 (with spaces)
**Expected Behavior:** ISBN should be normalized to `9780123456789`
This test demonstrates that ISBNs with spaces are automatically normalized.
}
@@ -0,0 +1,46 @@
meta {
name: Create Ebook - No Library Error
type: http
seq: 11
}
post {
url: {{base_url}}/api/ebooks
body: json
auth: inherit
}
body:json {
{
"title": "Test Ebook",
"isbn": "9780123456789",
"description": "Testing error when no library exists",
"file_path": "/path/to/ebook.epub",
"file_size": 1048576,
"mime_type": "application/epub+zip"
}
}
settings {
encodeUrl: true
timeout: 0
}
docs {
## Create Ebook - No Library Error
**Expected Behavior:** Should return 400 Bad Request with error message: "no ebook library found. Please create an ebook library first"
This test demonstrates the graceful error handling when attempting to create an ebook without first creating an ebook library.
**Setup Required:**
- Ensure NO ebook library exists in the database
- Run as admin user
**Expected Response:**
```json
{
"error": "no ebook library found. Please create an ebook library first"
}
```
}
+18 -11
View File
@@ -14,7 +14,7 @@ body:json {
{
"title": "Sample Ebook Title",
"author": "Author Name",
"isbn": "978-0123456789",
"isbn": "9780123456789",
"description": "A sample ebook description",
"file_path": "/path/to/ebook.epub",
"file_size": 1048576,
@@ -37,19 +37,19 @@ settings {
docs {
## Create Ebook
Creates a new ebook entry in the database.
**Method:** POST
**Endpoint:** /api/ebooks
**Authentication:** Required (Admin only)
**Request Body:** JSON object with:
- `title` (string, required): Ebook title (1-500 characters)
- `author` (string, optional): Author name
- `isbn` (string, optional): ISBN number
- `isbn` (string, optional): ISBN number (will be normalized to remove hyphens and spaces)
- `description` (string, optional): Ebook description
- `file_path` (string, required): Path to ebook file
- `file_size` (integer, required): File size in bytes
@@ -62,19 +62,26 @@ docs {
- `date_published` (string, optional): Publication date (YYYY-MM-DD)
- `publisher` (string, optional): Publisher name
- `contributors` (string, optional): Contributors list
**Response:** Complete ebook object with all fields
**Status Codes:**
- 201: Ebook created successfully
- 400: Bad request (invalid data)
- 400: Bad request (invalid data, or no ebook library exists)
- 401: Unauthorized
- 403: Forbidden (admin access required)
- 500: Internal server error
**Features:**
- Admin-only endpoint for creating ebooks
- Full validation of required fields
- Supports all ebook metadata fields
- ISBN normalization: automatically removes hyphens and spaces (e.g., "978-0-12345-678-9" becomes "9780123456789")
- Requires an existing ebook library to create ebooks
- Associates ebook with creating admin user
**ISBN Formats Supported:**
- ISBN-13: `9780123456789` or `978-0-12345-678-9` or `978-0123456789`
- ISBN-10: `0123456789` or `0-12345-678-9`
- All formats are automatically normalized (hyphens and spaces removed)
}
+506
View File
@@ -0,0 +1,506 @@
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
// TestISBNNormalization tests ISBN normalization with various formats
func TestISBNNormalization(t *testing.T) {
t.Run("ISBN-13 with hyphens should be normalized", func(t *testing.T) {
// Test cases for ISBN-13 with various hyphen placements
testCases := []struct {
name string
input string
expected string
}{
{
name: "ISBN-13 with hyphens (978-0-12345-678-9)",
input: "978-0-12345-678-9",
expected: "9780123456789",
},
{
name: "ISBN-13 with single hyphen (978-0123456789)",
input: "978-0123456789",
expected: "9780123456789",
},
{
name: "ISBN-13 without hyphens",
input: "9780123456789",
expected: "9780123456789",
},
{
name: "ISBN-13 with spaces",
input: "978 0123456789",
expected: "9780123456789",
},
{
name: "ISBN-13 with mixed hyphens and spaces",
input: "978-0 1234-56789",
expected: "9780123456789",
},
{
name: "ISBN-10 with hyphens",
input: "0-12345-678-9",
expected: "0123456789",
},
{
name: "ISBN-10 without hyphens",
input: "0123456789",
expected: "0123456789",
},
{
name: "ISBN-10 with X",
input: "0-12345-678-X",
expected: "012345678X",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
payload := map[string]interface{}{
"title": "Test Ebook",
"isbn": tc.input,
"file_path": "/path/to/file.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
req.Header.Set("X-User-Id", uuid.New().String())
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate successful creation with normalized ISBN
w.WriteHeader(http.StatusCreated)
response := map[string]interface{}{
"id": uuid.New().String(),
"title": "Test Ebook",
"isbn": tc.expected,
}
json.NewEncoder(w).Encode(response)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &response)
assert.Equal(t, tc.expected, response["isbn"])
})
}
})
t.Run("Empty ISBN should be handled", func(t *testing.T) {
payload := map[string]interface{}{
"title": "Test Ebook",
"file_path": "/path/to/file.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
})
t.Run("Invalid ISBN format should still be stored as-is", func(t *testing.T) {
// Test that very short or obviously invalid ISBNs are still accepted
// The database function will normalize what it can
testCases := []struct {
name string
isbn string
valid bool
}{
{"Too short ISBN", "123", true},
{"Valid ISBN-13", "9780123456789", true},
{"Valid ISBN-10", "0123456789", true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
payload := map[string]interface{}{
"title": "Test Ebook",
"isbn": tc.isbn,
"file_path": "/path/to/file.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tc.valid {
w.WriteHeader(http.StatusCreated)
} else {
w.WriteHeader(http.StatusBadRequest)
}
})
handler.ServeHTTP(rr, req)
if tc.valid {
assert.Equal(t, http.StatusCreated, rr.Code)
} else {
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
})
}
})
}
// TestEbookLibraryRequirement tests that ebooks require an existing library
func TestEbookLibraryRequirement(t *testing.T) {
t.Run("Create ebook without any libraries should fail gracefully", func(t *testing.T) {
payload := map[string]interface{}{
"title": "Test Ebook",
"isbn": "978-0123456789",
"file_path": "/path/to/file.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
req.Header.Set("X-User-Id", uuid.New().String())
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate no library found scenario
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error":"no ebook library found. Please create an ebook library first"}`))
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "no ebook library found")
})
t.Run("Create ebook with existing library should succeed", func(t *testing.T) {
libraryID := uuid.New()
payload := map[string]interface{}{
"title": "Test Ebook",
"isbn": "978-0123456789",
"file_path": "/path/to/file.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
req.Header.Set("X-User-Id", uuid.New().String())
req.Header.Set("X-Library-Id", libraryID.String())
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate successful creation
w.WriteHeader(http.StatusCreated)
response := map[string]interface{}{
"id": uuid.New().String(),
"title": "Test Ebook",
"isbn": "9780123456789",
"library_id": libraryID.String(),
}
json.NewEncoder(w).Encode(response)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &response)
assert.Equal(t, "9780123456789", response["isbn"])
assert.Equal(t, libraryID.String(), response["library_id"])
})
t.Run("Update ebook with normalized ISBN", func(t *testing.T) {
ebookID := uuid.New()
payload := map[string]interface{}{
"title": "Updated Ebook",
"isbn": "978-987654321-0",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("PUT", "/api/ebooks/"+ebookID.String(), bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
response := map[string]interface{}{
"id": ebookID.String(),
"title": "Updated Ebook",
"isbn": "9789876543210",
}
json.NewEncoder(w).Encode(response)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &response)
assert.Equal(t, "9789876543210", response["isbn"])
})
}
// TestISBNEdgeCases tests edge cases for ISBN handling
func TestISBNEdgeCases(t *testing.T) {
t.Run("ISBN with special characters should be normalized", func(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{
name: "ISBN with multiple hyphens",
input: "978-0-123-45678-9",
expected: "9780123456789",
},
{
name: "ISBN with trailing hyphen",
input: "9780123456789-",
expected: "9780123456789",
},
{
name: "ISBN with leading hyphen",
input: "-9780123456789",
expected: "9780123456789",
},
{
name: "ISBN with multiple spaces",
input: "978 0123456789",
expected: "9780123456789",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
payload := map[string]interface{}{
"title": "Test Ebook",
"isbn": tc.input,
"file_path": "/path/to/file.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
response := map[string]interface{}{
"id": uuid.New().String(),
"title": "Test Ebook",
"isbn": tc.expected,
}
json.NewEncoder(w).Encode(response)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &response)
assert.Equal(t, tc.expected, response["isbn"])
})
}
})
t.Run("ISBN length validation", func(t *testing.T) {
testCases := []struct {
name string
isbn string
shouldAccept bool
}{
{
name: "Empty ISBN",
isbn: "",
shouldAccept: true,
},
{
name: "Valid ISBN-10",
isbn: "0123456789",
shouldAccept: true,
},
{
name: "Valid ISBN-13",
isbn: "9780123456789",
shouldAccept: true,
},
{
name: "ISBN-13 with hyphens",
isbn: "978-0-12345-678-9",
shouldAccept: true,
},
{
name: "Maximum length (17 chars with hyphens)",
isbn: "978-0-12345-678-9",
shouldAccept: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
payload := map[string]interface{}{
"title": "Test Ebook",
"isbn": tc.isbn,
"file_path": "/path/to/file.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tc.shouldAccept {
w.WriteHeader(http.StatusCreated)
} else {
w.WriteHeader(http.StatusBadRequest)
}
})
handler.ServeHTTP(rr, req)
if tc.shouldAccept {
assert.Equal(t, http.StatusCreated, rr.Code)
} else {
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
})
}
})
}
// TestLibraryAutoSelection tests automatic library selection by resource type
func TestLibraryAutoSelection(t *testing.T) {
t.Run("Auto-select first available ebook library", func(t *testing.T) {
libraryID := uuid.New()
payload := map[string]interface{}{
"title": "Test Ebook",
"isbn": "9780123456789",
"file_path": "/path/to/file.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
req.Header.Set("X-User-Id", uuid.New().String())
req.Header.Set("X-Library-Id", libraryID.String())
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate auto-selection of first ebook library
w.WriteHeader(http.StatusCreated)
response := map[string]interface{}{
"id": uuid.New().String(),
"title": "Test Ebook",
"isbn": "9780123456789",
"library_id": libraryID.String(),
}
json.NewEncoder(w).Encode(response)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &response)
assert.Equal(t, libraryID.String(), response["library_id"])
})
t.Run("Multiple ebook libraries - should select first", func(t *testing.T) {
firstLibraryID := uuid.New()
secondLibraryID := uuid.New()
payload := map[string]interface{}{
"title": "Test Ebook",
"isbn": "9780123456789",
"file_path": "/path/to/file.epub",
"file_size": 1024,
"mime_type": "application/epub+zip",
}
jsonData, _ := json.Marshal(payload)
req := httptest.NewRequest("POST", "/api/ebooks", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer admin-token")
req.Header.Set("X-User-Role", "admin")
req.Header.Set("X-User-Id", uuid.New().String())
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate auto-selection of first ebook library
w.WriteHeader(http.StatusCreated)
response := map[string]interface{}{
"id": uuid.New().String(),
"title": "Test Ebook",
"isbn": "9780123456789",
"library_id": firstLibraryID.String(),
}
json.NewEncoder(w).Encode(response)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response map[string]interface{}
json.Unmarshal(rr.Body.Bytes(), &response)
// Should select first library, not second
assert.Equal(t, firstLibraryID.String(), response["library_id"])
assert.NotEqual(t, secondLibraryID.String(), response["library_id"])
})
}
+29 -1
View File
@@ -73,13 +73,25 @@ CREATE TABLE library_visibility (
UNIQUE(user_id, library_id)
);
-- ISBN normalization function
-- Removes hyphens and spaces from ISBN to standardize format
CREATE OR REPLACE FUNCTION normalize_isbn(isbn TEXT) RETURNS TEXT AS $$
BEGIN
IF isbn IS NULL THEN
RETURN NULL;
END IF;
-- Remove hyphens and spaces, return only digits and X (for ISBN-10)
RETURN regexp_replace(isbn, '[-\s]', '', 'g');
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- Create media_items table (replaces ebooks table for broader media support)
CREATE TABLE media_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
author VARCHAR(255),
isbn VARCHAR(13), -- Still relevant for ebooks
isbn VARCHAR(17), -- Supports ISBN-13 with hyphens (up to 17 chars)
description TEXT,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
@@ -195,6 +207,22 @@ WHERE lt.name = 'ebooks';
-- Note: user_ebook_folders table is replaced by library_folders table
-- Libraries now handle folder management instead of individual users
-- ISBN normalization trigger
-- Automatically normalizes ISBN on insert and update
CREATE OR REPLACE FUNCTION normalize_media_item_isbn() RETURNS TRIGGER AS $$
BEGIN
IF NEW.isbn IS NOT NULL THEN
NEW.isbn := normalize_isbn(NEW.isbn);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_normalize_media_item_isbn
BEFORE INSERT OR UPDATE ON media_items
FOR EACH ROW
EXECUTE FUNCTION normalize_media_item_isbn();
-- Create indexes for better query performance
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
+1
View File
@@ -52,6 +52,7 @@ type Querier interface {
GetEbookByFilePath(ctx context.Context, filePath string) (Ebooks, error)
GetEbookHighlight(ctx context.Context, id pgtype.UUID) (EbookHighlights, error)
GetEbookHighlights(ctx context.Context, arg GetEbookHighlightsParams) ([]EbookHighlights, error)
GetEbookLibraryID(ctx context.Context) (pgtype.UUID, error)
GetEbookNote(ctx context.Context, id pgtype.UUID) (EbookNotes, error)
GetEbookNotes(ctx context.Context, arg GetEbookNotesParams) ([]EbookNotes, error)
GetEbookRating(ctx context.Context, arg GetEbookRatingParams) (EbookRatings, error)
+19 -8
View File
@@ -44,14 +44,14 @@ func (q *Queries) CleanupExpiredRefreshTokens(ctx context.Context) error {
const CreateEbook = `-- name: CreateEbook :one
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id)
VALUES ((SELECT id FROM libraries WHERE library_type_id = (SELECT id FROM library_types WHERE name = 'ebooks') LIMIT 1), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
VALUES ((SELECT id FROM libraries WHERE library_type_id = (SELECT id FROM library_types WHERE name = 'ebooks') LIMIT 1), $1, $2, normalize_isbn($3), $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at
`
type CreateEbookParams struct {
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Isbn string `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
FilePath string `db:"file_path" json:"file_path"`
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
@@ -299,7 +299,7 @@ func (q *Queries) CreateMediaHighlight(ctx context.Context, arg CreateMediaHighl
const CreateMediaItem = `-- name: CreateMediaItem :one
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
VALUES ($1, $2, $3, normalize_isbn($4), $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at
`
@@ -307,7 +307,7 @@ type CreateMediaItemParams struct {
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Isbn string `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
FilePath string `db:"file_path" json:"file_path"`
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
@@ -781,6 +781,17 @@ func (q *Queries) GetEbookHighlights(ctx context.Context, arg GetEbookHighlights
return items, nil
}
const GetEbookLibraryID = `-- name: GetEbookLibraryID :one
SELECT id FROM libraries WHERE library_type_id = (SELECT id FROM library_types WHERE name = 'ebooks') LIMIT 1
`
func (q *Queries) GetEbookLibraryID(ctx context.Context) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, GetEbookLibraryID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const GetEbookNote = `-- name: GetEbookNote :one
SELECT id, media_item_id, user_id, content, position, created_at, updated_at, ebook_id FROM ebook_notes WHERE id = $1
`
@@ -1978,7 +1989,7 @@ const UpdateEbook = `-- name: UpdateEbook :one
UPDATE media_items SET
title = $2,
author = $3,
isbn = $4,
isbn = normalize_isbn($4),
description = $5,
cover_image_path = $6,
series = $7,
@@ -1997,7 +2008,7 @@ type UpdateEbookParams struct {
ID pgtype.UUID `db:"id" json:"id"`
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Isbn string `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
Series pgtype.Text `db:"series" json:"series"`
@@ -2249,7 +2260,7 @@ const UpdateMediaItem = `-- name: UpdateMediaItem :one
UPDATE media_items SET
title = $2,
author = $3,
isbn = $4,
isbn = normalize_isbn($4),
description = $5,
cover_image_path = $6,
series = $7,
@@ -2268,7 +2279,7 @@ type UpdateMediaItemParams struct {
ID pgtype.UUID `db:"id" json:"id"`
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Isbn string `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
Series pgtype.Text `db:"series" json:"series"`
+7 -4
View File
@@ -98,7 +98,7 @@ ORDER BY l.created_at DESC;
-- Media Items queries
-- name: CreateMediaItem :one
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
VALUES ($1, $2, $3, normalize_isbn($4), $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
RETURNING *;
-- name: GetMediaItem :one
@@ -123,7 +123,7 @@ ORDER BY mi.created_at DESC;
UPDATE media_items SET
title = $2,
author = $3,
isbn = $4,
isbn = normalize_isbn($4),
description = $5,
cover_image_path = $6,
series = $7,
@@ -150,16 +150,19 @@ SELECT * FROM ebooks WHERE id = $1;
-- name: ListEbooks :many
SELECT * FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2;
-- name: GetEbookLibraryID :one
SELECT id FROM libraries WHERE library_type_id = (SELECT id FROM library_types WHERE name = 'ebooks') LIMIT 1;
-- name: CreateEbook :one
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id)
VALUES ((SELECT id FROM libraries WHERE library_type_id = (SELECT id FROM library_types WHERE name = 'ebooks') LIMIT 1), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
VALUES ((SELECT id FROM libraries WHERE library_type_id = (SELECT id FROM library_types WHERE name = 'ebooks') LIMIT 1), $1, $2, normalize_isbn($3), $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
RETURNING *;
-- name: UpdateEbook :one
UPDATE media_items SET
title = $2,
author = $3,
isbn = $4,
isbn = normalize_isbn($4),
description = $5,
cover_image_path = $6,
series = $7,
+11
View File
@@ -232,6 +232,17 @@ func (h *Handler) CreateEbook(c echo.Context) error {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
}
// Check if an ebook library exists before attempting to create an ebook
_, err = h.db.GetEbookLibraryID(c.Request().Context())
if err != nil {
if err == pgx.ErrNoRows {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "no ebook library found. Please create an ebook library first",
})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
ebook, err := h.db.CreateEbook(c.Request().Context(), database.CreateEbookParams{
Title: req.Title,
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},