Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Dependencies
node_modules/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/

# Database
*.db
*.sqlite
*.sqlite3

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Logs
*.log
logs/

# Environment variables
.env
.env.local
.env.production.local
.env.test.local

# Build outputs
.next/
dist/
build/

# Cache
.cache/
.parcel-cache/
132 changes: 130 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,130 @@
# ai-book-reader
AI assisted book reader
# AI Book Reader

AI assisted book reader with smart notes and bookmarks management.

## Features

- 📝 **Smart Notes**: Take AI-powered notes while reading
- 🔖 **Bookmarks**: Save important passages and references
- 🤖 **AI Insights**: Get AI-generated summaries and insights (coming soon)
- 💾 **SQLite Database**: Local storage for notes and bookmarks

## Tech Stack

### Frontend
- **Next.js 15** with TypeScript
- **React 19**
- **Tailwind CSS** for styling
- **App Router** for navigation

### Backend
- **FastAPI** with Python 3.12
- **SQLAlchemy** for database ORM
- **SQLite** for data storage
- **Pydantic** for data validation
- **CORS** enabled for frontend integration

## Project Structure

```
ai-book-reader/
├── frontend/ # Next.js 15 frontend application
│ ├── app/ # App router pages and components
│ │ ├── notes/ # Notes management page
│ │ ├── bookmarks/# Bookmarks management page
│ │ └── ...
│ └── package.json # Frontend dependencies
├── backend/ # FastAPI backend application
│ ├── app/ # Main application code
│ │ ├── main.py # FastAPI app and routes
│ │ ├── models.py# SQLAlchemy database models
│ │ ├── schemas.py# Pydantic schemas
│ │ └── database.py# Database configuration
│ └── requirements.txt# Backend dependencies
├── vercel.json # Vercel deployment configuration
└── start.sh # Local development script
```

## Getting Started

### Prerequisites
- Node.js 20+
- Python 3.12+
- npm or yarn
Comment on lines +51 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify Python version requirement.

The README specifies Python 3.12+ as a prerequisite. Ensure this matches the actual Python version used in development and any version constraints in the backend code or deployment configuration.


🏁 Script executed:

#!/bin/bash
# Description: Check for Python version specifications in backend config files

# Search for Python version specifications in backend files
rg -n "python_requires|python-version|python_version" backend/

# Check for Python version in vercel.json or other deployment configs
rg -n "python|runtime" vercel.json 2>/dev/null || echo "vercel.json not found in context"

Length of output: 32


🏁 Script executed:

#!/bin/bash
# Locate and preview Python version declarations in project config files

# Find relevant config files in backend/
config_files=$(find backend -maxdepth 2 -type f \( -name 'pyproject.toml' -o -name 'Pipfile' -o -name 'setup.py' -o -name 'setup.cfg' \))

if [ -z "$config_files" ]; then
  echo "No pyproject.toml, Pipfile, setup.py, or setup.cfg found in backend/"
else
  for file in $config_files; do
    echo "---- $file ----"
    sed -n '1,200p' "$file"
  done
fi

Length of output: 68


Enforce or align the Python 3.12+ requirement
No Python version constraint is declared in your configs. Add a pythonVersion: "3.12" entry under the Python builder in vercel.json (or include a runtime.txt/CI setting) to enforce 3.12+, or adjust the README to reflect the actual supported version.

🤖 Prompt for AI Agents
In README.md around lines 51 to 53, the README claims "Python 3.12+" but no
project configs enforce that; add a pythonVersion: "3.12" entry under the Python
builder in vercel.json (or add runtime.txt with "python-3.12" or update CI
config) to enforce Python 3.12+, or alternatively update the README to reflect
the actual supported Python version if you cannot or do not want to require
3.12.


### Installation

1. **Install Frontend Dependencies**
```bash
cd frontend
npm install
```

2. **Install Backend Dependencies**
```bash
cd backend
pip install -r requirements.txt
```

### Development

#### Option 1: Start Both Servers with Script
```bash
chmod +x start.sh
./start.sh
```

#### Option 2: Start Servers Individually

**Backend (Terminal 1):**
```bash
cd backend
python3 -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```

**Frontend (Terminal 2):**
```bash
cd frontend
npm run dev
```

### Access the Application

- **Frontend**: http://localhost:3000
- **Backend API**: http://localhost:8000
- **API Documentation**: http://localhost:8000/docs

## API Endpoints

### Notes
- `GET /notes/` - List all notes
- `POST /notes/` - Create a new note
- `GET /notes/{id}` - Get specific note
- `PUT /notes/{id}` - Update note
- `DELETE /notes/{id}` - Delete note

### Bookmarks
- `GET /bookmarks/` - List all bookmarks
- `POST /bookmarks/` - Create a new bookmark
- `GET /bookmarks/{id}` - Get specific bookmark
- `DELETE /bookmarks/{id}` - Delete bookmark

## Deployment

This project is configured for deployment on **Vercel**:

1. Connect your GitHub repository to Vercel
2. Vercel will automatically detect the configuration from `vercel.json`
3. Both frontend and backend will be deployed as serverless functions

## Contributing

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test locally
5. Submit a pull request

## License

This project is open source and available under the MIT License.
28 changes: 28 additions & 0 deletions api/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(
title="AI Book Reader API",
description="API for managing notes and bookmarks in the AI Book Reader",
version="1.0.0"
)
Comment on lines +1 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify consistency with backend/app/main.py.

This file creates a separate FastAPI app instance, but the PR context indicates that backend/app/main.py contains the full CRUD API. Ensure these are not duplicate/conflicting app instances.

Run the following script to check for duplicate FastAPI app instances:


🏁 Script executed:

#!/bin/bash
# Description: Find all FastAPI app instance declarations

rg -n "app = FastAPI\(" --type=py

Length of output: 67


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' api/index.py
echo "---"
sed -n '1,200p' backend/app/main.py

Length of output: 4384


Consolidate FastAPI app definition
Remove the duplicate app = FastAPI(...) in api/index.py and instead import or include the existing app (or its routers) from backend/app/main.py to avoid conflicting FastAPI instances.

🤖 Prompt for AI Agents
In api/index.py around lines 1 to 8 you are creating a second FastAPI instance
which conflicts with the existing app in backend/app/main.py; remove the
FastAPI(...) instantiation here and instead import the existing app (from
backend.app.main import app) or import and include the routers from
backend.app.main (from backend.app.main import router;
app.include_router(router)) so this module re-uses the main application rather
than creating a duplicate instance.


# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
Comment on lines +11 to +15

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid CORS wildcard with credentials

Initializing CORSMiddleware with allow_origins=["*"] while allow_credentials=True causes Starlette to raise ValueError at startup (“Cannot use allow_credentials True when allow_origins=['*']”). As written, the minimal API under api/ cannot even start, so any deployment that imports this module will crash before serving requests. Consider either listing specific origins or disabling credentials.

Useful? React with 👍 / 👎.

allow_headers=["*"],
)
Comment on lines +11 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical security issue: Overly permissive CORS configuration.

The CORS configuration allows all origins (["*"]) with allow_credentials=True, which is a security vulnerability. This configuration:

  • Exposes the API to CSRF attacks from any origin
  • Violates the CORS specification (browsers reject Access-Control-Allow-Origin: * when credentials are included)
  • Should be restricted to specific, trusted origins in production

Apply this diff to fix the security issue:

 app.add_middleware(
     CORSMiddleware,
-    allow_origins=["*"],
+    allow_origins=[
+        "http://localhost:3000",  # Local development
+        # Add production frontend URLs here
+    ],
     allow_credentials=True,
-    allow_methods=["*"],
-    allow_headers=["*"],
+    allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
+    allow_headers=["Content-Type", "Authorization"],
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # Local development
# Add production frontend URLs here
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["Content-Type", "Authorization"],
)
🤖 Prompt for AI Agents
In api/index.py around lines 11 to 17 the CORS setup is overly permissive
(allow_origins=["*"] with allow_credentials=True). Replace the wildcard origin
with a restricted list of trusted origins loaded from configuration or an
environment variable (e.g., ALLOWED_ORIGINS or config.ALLOWED_ORIGINS), ensure
allow_credentials remains True only when specific origins are provided (never
use "*" with credentials), and if running in non-production allow a development
fallback (e.g., localhost origins) while enforcing strict origins in production;
update tests/config docs accordingly.


@app.get("/")
def read_root():
return {"message": "AI Book Reader API", "version": "1.0.0"}

@app.get("/health")
def health_check():
return {"status": "healthy"}

# Export the app for Vercel
handler = app
1 change: 1 addition & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# AI Book Reader Backend Package
5 changes: 5 additions & 0 deletions backend/api/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from app.main import app

# This is the Vercel serverless function handler
def handler(request, response):
return app(request, response)
Comment on lines +1 to +5
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Incorrect serverless handler implementation for FastAPI.

This handler implementation has two critical issues:

  1. Incorrect ASGI invocation: FastAPI is an ASGI application and cannot be invoked directly as app(request, response). ASGI apps require an ASGI server (like Uvicorn) or proper ASGI-to-HTTP adapter.

  2. Import path assumption: The relative import from app.main import app assumes execution from the backend/ directory, but Vercel serverless functions execute from the project root, which will cause import failures.

For Vercel deployment of FastAPI, use the Mangum adapter which properly bridges ASGI to AWS Lambda/Vercel's handler signature:

-from app.main import app
+from mangum import Mangum
+from backend.app.main import app

-# This is the Vercel serverless function handler
-def handler(request, response):
-    return app(request, response)
+# Vercel serverless function handler using Mangum adapter
+handler = Mangum(app, lifespan="off")

Add mangum to backend/requirements.txt:

 fastapi>=0.115.0
 uvicorn[standard]>=0.32.0
 sqlalchemy>=2.0.36
 pydantic>=2.10.0
 python-multipart>=0.0.17
 httpx>=0.28.0
+mangum>=0.19.0

Verify the Vercel configuration routes to this handler correctly:

#!/bin/bash
# Description: Check Vercel configuration for API routing

echo "=== Checking vercel.json for API routing configuration ==="
if [ -f "vercel.json" ]; then
  cat vercel.json | jq '.routes, .rewrites, .builds' 2>/dev/null || cat vercel.json
else
  echo "vercel.json not found"
fi
🤖 Prompt for AI Agents
In backend/api/main.py lines 1-5, the handler incorrectly calls the FastAPI app
directly and uses a relative import that will fail from the project root;
replace this with the Mangum ASGI adapter and an absolute import: import the
FastAPI app using its package-absolute path (matching the project layout) and
create a Mangum adapter instance (e.g., handler = Mangum(app)) to expose as the
serverless entrypoint; also add mangum to backend/requirements.txt and verify
vercel.json routes/rewrites point to backend/api/main.py as the API entry so
Vercel invokes the correct handler.

1 change: 1 addition & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# AI Book Reader Backend
24 changes: 24 additions & 0 deletions backend/app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused import.

The os module is imported but never used.

 from sqlalchemy import create_engine
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import sessionmaker
-import os
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import os
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
🤖 Prompt for AI Agents
In backend/app/database.py around line 4, the os module is imported but never
used; remove the unused import line "import os" from the top of the file to
eliminate the unused dependency and any related linter warnings.


# SQLite database URL
SQLALCHEMY_DATABASE_URL = "sqlite:///./ai_book_reader.db"

# Create SQLite engine
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
Comment on lines +6 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Make database URL configurable via environment variables.

The database URL is hardcoded, which prevents using different databases for development, testing, and production environments.

+import os
+
 # SQLite database URL
-SQLALCHEMY_DATABASE_URL = "sqlite:///./ai_book_reader.db"
+SQLALCHEMY_DATABASE_URL = os.getenv(
+    "DATABASE_URL",
+    "sqlite:///./ai_book_reader.db"
+)

 # Create SQLite engine
 engine = create_engine(
-    SQLALCHEMY_DATABASE_URL, 
-    connect_args={"check_same_thread": False}
+    SQLALCHEMY_DATABASE_URL,
+    connect_args={"check_same_thread": False} if SQLALCHEMY_DATABASE_URL.startswith("sqlite") else {}
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# SQLite database URL
SQLALCHEMY_DATABASE_URL = "sqlite:///./ai_book_reader.db"
# Create SQLite engine
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
import os
# SQLite database URL
SQLALCHEMY_DATABASE_URL = os.getenv(
"DATABASE_URL",
"sqlite:///./ai_book_reader.db"
)
# Create SQLite engine
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False} if SQLALCHEMY_DATABASE_URL.startswith("sqlite") else {}
)
🤖 Prompt for AI Agents
In backend/app/database.py around lines 6 to 13, the database URL is hardcoded;
update the file to read the database URL from an environment variable (e.g.,
DATABASE_URL) with the current sqlite value as a safe default, and use that
value when building the SQLAlchemy engine; ensure
connect_args={"check_same_thread": False} is only passed for SQLite (detect by
checking if the URL starts with "sqlite://") so other DBs (Postgres, MySQL) are
created without SQLite-specific args.


SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
108 changes: 108 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from typing import List, Optional

from . import models, schemas, database
from .database import get_db

# Create database tables
models.Base.metadata.create_all(bind=database.engine)
Comment on lines +9 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace with proper migration tooling for production.

Creating tables at module import time is acceptable for initial development but has significant limitations:

  • No schema version control or migration history.
  • Concurrent deployments (multiple serverless instances on Vercel) may race to create tables.
  • Schema changes require manual intervention or risk data loss.

For production readiness, integrate Alembic to manage migrations with versioning and rollback capabilities.

Would you like me to generate an Alembic setup with initial migration scripts?


app = FastAPI(
title="AI Book Reader API",
description="API for managing notes and bookmarks in the AI Book Reader",
version="1.0.0"
)

# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "https://*.vercel.app"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
Comment on lines +19 to +24

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge CORS config blocks all Vercel subdomains

The backend CORS setup lists "https://*.vercel.app" in allow_origins, but Starlette treats entries as literal strings, not glob patterns. As a result, an origin such as https://myapp.vercel.app will not match and browsers will reject cross‑origin requests, preventing the deployed Next.js frontend from calling the API. Use allow_origin_regex=r"https://.*\.vercel\.app" or enumerate allowed origins instead of a wildcard string.

Useful? React with 👍 / 👎.

)
Comment on lines +18 to +25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix CORS configuration: wildcard origins incompatible with credentials.

The current CORS configuration has a critical flaw: browsers reject wildcard origins (https://*.vercel.app) when allow_credentials=True. This will cause CORS errors in production.

Additionally, allowing all methods and headers (["*"]) is overly permissive and expands the attack surface.

Solution 1 (recommended): Use specific allowed origins

-    allow_origins=["http://localhost:3000", "https://*.vercel.app"],
+    allow_origins=[
+        "http://localhost:3000",
+        "https://your-app.vercel.app",  # Replace with actual domain
+    ],
     allow_credentials=True,
-    allow_methods=["*"],
-    allow_headers=["*"],
+    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
+    allow_headers=["Content-Type", "Authorization"],

Solution 2: Dynamic origin validation (if multiple Vercel deployments needed)

from fastapi.middleware.cors import CORSMiddleware
import re

def is_allowed_origin(origin: str) -> bool:
    allowed_patterns = [
        r"^http://localhost:3000$",
        r"^https://.*\.vercel\.app$",
    ]
    return any(re.match(pattern, origin) for pattern in allowed_patterns)

app.add_middleware(
    CORSMiddleware,
    allow_origin_regex=r"^(http://localhost:3000|https://.*\.vercel\.app)$",
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization"],
)
🤖 Prompt for AI Agents
In backend/app/main.py around lines 18-25, the CORS config uses a wildcard
origin ("https://*.vercel.app") together with allow_credentials=True and overly
permissive methods/headers; browsers will reject wildcard origins with
credentials and the current allow_methods/allow_headers are too broad. Replace
the wildcard origin with either a concrete list of allowed origins or use
allow_origin_regex to validate Vercel subdomains (ensuring the regex matches
only intended hosts), keep allow_credentials=True only with these specific/or
regex-validated origins, and restrict allow_methods to the explicit HTTP verbs
your API uses (e.g., GET, POST, PUT, DELETE, OPTIONS) and allow_headers to only
required headers (e.g., Content-Type, Authorization) instead of ["*"].


@app.get("/")
def read_root():
return {"message": "AI Book Reader API", "version": "1.0.0"}

@app.get("/health")
def health_check():
return {"status": "healthy"}

# Notes endpoints
@app.post("/notes/", response_model=schemas.Note)
def create_note(note: schemas.NoteCreate, db: Session = Depends(get_db)):
db_note = models.Note(**note.dict())
db.add(db_note)
db.commit()
db.refresh(db_note)
return db_note
Comment on lines +37 to +42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace deprecated .dict() with .model_dump().

The .dict() method is deprecated in Pydantic v2 (used by FastAPI 0.104.1+). Use .model_dump() instead to avoid future compatibility issues.

Apply this diff:

 def create_note(note: schemas.NoteCreate, db: Session = Depends(get_db)):
-    db_note = models.Note(**note.dict())
+    db_note = models.Note(**note.model_dump())
     db.add(db_note)
     db.commit()
     db.refresh(db_note)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def create_note(note: schemas.NoteCreate, db: Session = Depends(get_db)):
db_note = models.Note(**note.dict())
db.add(db_note)
db.commit()
db.refresh(db_note)
return db_note
def create_note(note: schemas.NoteCreate, db: Session = Depends(get_db)):
db_note = models.Note(**note.model_dump())
db.add(db_note)
db.commit()
db.refresh(db_note)
return db_note
🧰 Tools
🪛 Ruff (0.13.2)

37-37: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
In backend/app/main.py around lines 37 to 42, replace the deprecated Pydantic v1
usage of note.dict() with the v2-compatible note.model_dump() when constructing
the Note model; update the call to models.Note(**note.model_dump()) so the code
uses model_dump() (optionally pass exclusions like exclude_none=True if needed)
and leave the rest of the DB add/commit/refresh logic unchanged.

⚠️ Potential issue | 🟠 Major

Add database error handling for constraint violations.

Database operations can fail due to constraint violations (unique keys, foreign keys, check constraints). Without error handling, these will return 500 errors with raw SQL exceptions exposed to clients.

Wrap database operations with exception handling:

+from sqlalchemy.exc import IntegrityError
+
 def create_note(note: schemas.NoteCreate, db: Session = Depends(get_db)):
-    db_note = models.Note(**note.model_dump())
-    db.add(db_note)
-    db.commit()
-    db.refresh(db_note)
-    return db_note
+    try:
+        db_note = models.Note(**note.model_dump())
+        db.add(db_note)
+        db.commit()
+        db.refresh(db_note)
+        return db_note
+    except IntegrityError as e:
+        db.rollback()
+        raise HTTPException(status_code=400, detail="Database constraint violation")

Apply similar error handling to all create, update, and delete operations.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.13.2)

37-37: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
In backend/app/main.py around lines 37 to 42, the create_note DB write lacks
error handling for constraint violations; wrap the add/commit/refresh sequence
in a try/except that catches SQLAlchemy exceptions (e.g.,
sqlalchemy.exc.IntegrityError and sqlalchemy.exc.DBAPIError), call db.rollback()
on exception, and raise an appropriate fastapi.HTTPException with a safe
client-facing status code and message (e.g., 400 or 409 with a concise
"constraint violation" note). Ensure the original exception is not leaked to the
client but is logged for debugging. Apply the same pattern (try/except,
rollback, HTTPException + logging) to all create, update, and delete DB
operations in the codebase.


@app.get("/notes/", response_model=List[schemas.Note])
def read_notes(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
notes = db.query(models.Note).offset(skip).limit(limit).all()
return notes

@app.get("/notes/{note_id}", response_model=schemas.Note)
def read_note(note_id: int, db: Session = Depends(get_db)):
note = db.query(models.Note).filter(models.Note.id == note_id).first()
if note is None:
raise HTTPException(status_code=404, detail="Note not found")
return note

@app.put("/notes/{note_id}", response_model=schemas.Note)
def update_note(note_id: int, note: schemas.NoteUpdate, db: Session = Depends(get_db)):
db_note = db.query(models.Note).filter(models.Note.id == note_id).first()
if db_note is None:
raise HTTPException(status_code=404, detail="Note not found")

for key, value in note.dict(exclude_unset=True).items():
setattr(db_note, key, value)

db.commit()
db.refresh(db_note)
return db_note

@app.delete("/notes/{note_id}")
def delete_note(note_id: int, db: Session = Depends(get_db)):
db_note = db.query(models.Note).filter(models.Note.id == note_id).first()
if db_note is None:
raise HTTPException(status_code=404, detail="Note not found")

db.delete(db_note)
db.commit()
return {"message": "Note deleted"}

# Bookmarks endpoints
@app.post("/bookmarks/", response_model=schemas.Bookmark)
def create_bookmark(bookmark: schemas.BookmarkCreate, db: Session = Depends(get_db)):
db_bookmark = models.Bookmark(**bookmark.dict())
db.add(db_bookmark)
db.commit()
db.refresh(db_bookmark)
return db_bookmark
Comment on lines +81 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace deprecated .dict() with .model_dump() and add error handling.

Same issues as the notes endpoints: use .model_dump() instead of the deprecated .dict() method, and add database error handling for constraint violations.

Apply this diff:

+from sqlalchemy.exc import IntegrityError
+
 def create_bookmark(bookmark: schemas.BookmarkCreate, db: Session = Depends(get_db)):
-    db_bookmark = models.Bookmark(**bookmark.dict())
-    db.add(db_bookmark)
-    db.commit()
-    db.refresh(db_bookmark)
-    return db_bookmark
+    try:
+        db_bookmark = models.Bookmark(**bookmark.model_dump())
+        db.add(db_bookmark)
+        db.commit()
+        db.refresh(db_bookmark)
+        return db_bookmark
+    except IntegrityError as e:
+        db.rollback()
+        raise HTTPException(status_code=400, detail="Database constraint violation")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def create_bookmark(bookmark: schemas.BookmarkCreate, db: Session = Depends(get_db)):
db_bookmark = models.Bookmark(**bookmark.dict())
db.add(db_bookmark)
db.commit()
db.refresh(db_bookmark)
return db_bookmark
from sqlalchemy.exc import IntegrityError
def create_bookmark(bookmark: schemas.BookmarkCreate, db: Session = Depends(get_db)):
try:
db_bookmark = models.Bookmark(**bookmark.model_dump())
db.add(db_bookmark)
db.commit()
db.refresh(db_bookmark)
return db_bookmark
except IntegrityError as e:
db.rollback()
raise HTTPException(status_code=400, detail="Database constraint violation")
🧰 Tools
🪛 Ruff (0.13.2)

81-81: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
In backend/app/main.py around lines 81 to 86, replace the deprecated
BookmarkCreate.dict() call with BookmarkCreate.model_dump(), and wrap the DB
operations (add/commit/refresh) in a try/except that catches SQLAlchemy
IntegrityError (or relevant DB constraint exception), calls db.rollback() on
error, and raises an appropriate fastapi.HTTPException (e.g., 400 or 409) with a
clear message; also add the necessary imports for IntegrityError and
HTTPException at the top of the file.


@app.get("/bookmarks/", response_model=List[schemas.Bookmark])
def read_bookmarks(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
bookmarks = db.query(models.Bookmark).offset(skip).limit(limit).all()
return bookmarks

@app.get("/bookmarks/{bookmark_id}", response_model=schemas.Bookmark)
def read_bookmark(bookmark_id: int, db: Session = Depends(get_db)):
bookmark = db.query(models.Bookmark).filter(models.Bookmark.id == bookmark_id).first()
if bookmark is None:
raise HTTPException(status_code=404, detail="Bookmark not found")
return bookmark

@app.delete("/bookmarks/{bookmark_id}")
def delete_bookmark(bookmark_id: int, db: Session = Depends(get_db)):
db_bookmark = db.query(models.Bookmark).filter(models.Bookmark.id == bookmark_id).first()
if db_bookmark is None:
raise HTTPException(status_code=404, detail="Bookmark not found")

db.delete(db_bookmark)
db.commit()
return {"message": "Bookmark deleted"}
27 changes: 27 additions & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean
from sqlalchemy.sql import func
from .database import Base

class Note(Base):
__tablename__ = "notes"

id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False, index=True)
content = Column(Text, nullable=False)
book_title = Column(String(255), nullable=False)
page_number = Column(Integer, nullable=True)
chapter = Column(String(255), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
Comment on lines +14 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix updated_at to include server_default.

The updated_at column has onupdate=func.now() but no server_default, so it will be NULL on initial insert. This creates inconsistent behavior where the field is only populated after the first update.

Apply this diff to fix the issue:

-    updated_at = Column(DateTime(timezone=True), onupdate=func.now())
+    updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
🤖 Prompt for AI Agents
In backend/app/models.py around lines 14 to 15, the updated_at column uses
onupdate=func.now() but lacks a server_default so it will be NULL on initial
insert; update the Column definition to include server_default=func.now() (and
optionally nullable=False) so the timestamp is set on create and on subsequent
updates, ensuring consistent non-NULL values.


class Bookmark(Base):
__tablename__ = "bookmarks"

id = Column(Integer, primary_key=True, index=True)
book_title = Column(String(255), nullable=False, index=True)
page_number = Column(Integer, nullable=False)
chapter = Column(String(255), nullable=True)
quote = Column(Text, nullable=True)
notes = Column(Text, nullable=True)
is_favorite = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
Loading