diff --git a/backend/app/crud/entries.py b/backend/app/crud/entries.py index 3f8455e..eae2e1c 100644 --- a/backend/app/crud/entries.py +++ b/backend/app/crud/entries.py @@ -1,3 +1,4 @@ +from nanoid import generate from sqlalchemy.orm import Session from app.core.logging import get_logger @@ -8,7 +9,7 @@ logger = get_logger(__name__) def get_entries_by_newsletter( - db: Session, newsletter_id: int, skip: int = 0, limit: int = 100 + db: Session, newsletter_id: str, skip: int = 0, limit: int = 100 ): """Retrieve entries for a specific newsletter.""" logger.debug( @@ -29,12 +30,12 @@ def get_entry_by_message_id(db: Session, message_id: str): return db.query(Entry).filter(Entry.message_id == message_id).first() -def create_entry(db: Session, entry: EntryCreate, newsletter_id: int): +def create_entry(db: Session, entry: EntryCreate, newsletter_id: str): """Create a new entry for a newsletter.""" logger.info( f"Creating new entry for newsletter_id={newsletter_id} with subject '{entry.subject}'" ) - db_entry = Entry(**entry.model_dump(), newsletter_id=newsletter_id) + db_entry = Entry(id=generate(), **entry.model_dump(), newsletter_id=newsletter_id) db.add(db_entry) db.commit() db.refresh(db_entry) diff --git a/backend/app/crud/newsletters.py b/backend/app/crud/newsletters.py index 54558fb..a9d29b6 100644 --- a/backend/app/crud/newsletters.py +++ b/backend/app/crud/newsletters.py @@ -1,3 +1,4 @@ +from nanoid import generate from sqlalchemy import func from sqlalchemy.orm import Session @@ -9,7 +10,7 @@ from app.schemas.newsletters import NewsletterCreate, NewsletterUpdate logger = get_logger(__name__) -def get_newsletter(db: Session, newsletter_id: int): +def get_newsletter(db: Session, newsletter_id: str): """Retrieve a single newsletter by its ID.""" logger.debug(f"Querying for newsletter with id={newsletter_id}") result = ( @@ -51,6 +52,7 @@ def create_newsletter(db: Session, newsletter: NewsletterCreate): """Create a new newsletter.""" logger.info(f"Creating new newsletter with name '{newsletter.name}'") db_newsletter = Newsletter( + id=generate(size=10), name=newsletter.name, extract_content=newsletter.extract_content, move_to_folder=newsletter.move_to_folder, @@ -60,7 +62,7 @@ def create_newsletter(db: Session, newsletter: NewsletterCreate): db.refresh(db_newsletter) for email in newsletter.sender_emails: - db_sender = Sender(email=email, newsletter_id=db_newsletter.id) + db_sender = Sender(id=generate(), email=email, newsletter_id=db_newsletter.id) db.add(db_sender) db.commit() @@ -72,7 +74,7 @@ def create_newsletter(db: Session, newsletter: NewsletterCreate): def update_newsletter( - db: Session, newsletter_id: int, newsletter_update: NewsletterUpdate + db: Session, newsletter_id: str, newsletter_update: NewsletterUpdate ): """Update an existing newsletter.""" logger.info(f"Updating newsletter with id={newsletter_id}") @@ -90,7 +92,7 @@ def update_newsletter( db.commit() for email in newsletter_update.sender_emails: - db_sender = Sender(email=email, newsletter_id=db_newsletter.id) + db_sender = Sender(id=generate(), email=email, newsletter_id=db_newsletter.id) db.add(db_sender) db.commit() @@ -99,7 +101,7 @@ def update_newsletter( return get_newsletter(db, newsletter_id) -def delete_newsletter(db: Session, newsletter_id: int): +def delete_newsletter(db: Session, newsletter_id: str): """Delete a newsletter by its ID.""" logger.info(f"Deleting newsletter with id={newsletter_id}") db_newsletter = get_newsletter(db, newsletter_id) diff --git a/backend/app/models/entries.py b/backend/app/models/entries.py index 6c1569a..269c096 100644 --- a/backend/app/models/entries.py +++ b/backend/app/models/entries.py @@ -1,6 +1,6 @@ import datetime -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy import Column, DateTime, ForeignKey, String, Text from sqlalchemy.orm import relationship from app.core.database import Base @@ -11,8 +11,8 @@ class Entry(Base): __tablename__ = "entries" - id = Column(Integer, primary_key=True, index=True) - newsletter_id = Column(Integer, ForeignKey("newsletters.id")) + id = Column(String, primary_key=True, index=True) + newsletter_id = Column(String, ForeignKey("newsletters.id")) subject = Column(String) body = Column(Text) received_at = Column( diff --git a/backend/app/models/newsletters.py b/backend/app/models/newsletters.py index c9c68d2..0047284 100644 --- a/backend/app/models/newsletters.py +++ b/backend/app/models/newsletters.py @@ -1,4 +1,4 @@ -from sqlalchemy import Boolean, Column, ForeignKey, Integer, String +from sqlalchemy import Boolean, Column, ForeignKey, String from sqlalchemy.orm import relationship from app.core.database import Base @@ -9,7 +9,7 @@ class Newsletter(Base): __tablename__ = "newsletters" - id = Column(Integer, primary_key=True, index=True) + id = Column(String, primary_key=True, index=True) name = Column(String) move_to_folder = Column(String, nullable=True) is_active = Column(Boolean, default=True) @@ -28,8 +28,8 @@ class Sender(Base): __tablename__ = "senders" - id = Column(Integer, primary_key=True, index=True) + id = Column(String, primary_key=True, index=True) email = Column(String, unique=True, index=True, nullable=False) - newsletter_id = Column(Integer, ForeignKey("newsletters.id"), nullable=False) + newsletter_id = Column(String, ForeignKey("newsletters.id"), nullable=False) newsletter = relationship("Newsletter", back_populates="senders") diff --git a/backend/app/routers/feeds.py b/backend/app/routers/feeds.py index 7bd2dc8..85ec9d1 100644 --- a/backend/app/routers/feeds.py +++ b/backend/app/routers/feeds.py @@ -11,7 +11,7 @@ router = APIRouter() @router.get("/feeds/{newsletter_id}") -def get_newsletter_feed(newsletter_id: int, db: Session = Depends(get_db)): +def get_newsletter_feed(newsletter_id: str, db: Session = Depends(get_db)): """Generate an Atom feed for a specific newsletter.""" logger.info(f"Generating feed for newsletter_id={newsletter_id}") feed = generate_feed(db, newsletter_id) diff --git a/backend/app/routers/newsletters.py b/backend/app/routers/newsletters.py index 3243447..b5c0b79 100644 --- a/backend/app/routers/newsletters.py +++ b/backend/app/routers/newsletters.py @@ -38,7 +38,7 @@ def read_newsletters(skip: int = 0, limit: int = 100, db: Session = Depends(get_ @router.get("/newsletters/{newsletter_id}", response_model=Newsletter) -def read_newsletter(newsletter_id: int, db: Session = Depends(get_db)): +def read_newsletter(newsletter_id: str, db: Session = Depends(get_db)): """Retrieve a single newsletter by its ID.""" logger.info(f"Request to read newsletter with id={newsletter_id}") db_newsletter = get_newsletter(db, newsletter_id=newsletter_id) @@ -50,7 +50,7 @@ def read_newsletter(newsletter_id: int, db: Session = Depends(get_db)): @router.put("/newsletters/{newsletter_id}", response_model=Newsletter) def update_existing_newsletter( - newsletter_id: int, newsletter: NewsletterUpdate, db: Session = Depends(get_db) + newsletter_id: str, newsletter: NewsletterUpdate, db: Session = Depends(get_db) ): """Update an existing newsletter.""" logger.info(f"Request to update newsletter with id={newsletter_id}") @@ -64,7 +64,7 @@ def update_existing_newsletter( @router.delete("/newsletters/{newsletter_id}", response_model=Newsletter) -def delete_existing_newsletter(newsletter_id: int, db: Session = Depends(get_db)): +def delete_existing_newsletter(newsletter_id: str, db: Session = Depends(get_db)): """Delete a newsletter by its ID.""" logger.info(f"Request to delete newsletter with id={newsletter_id}") db_newsletter = delete_newsletter(db, newsletter_id=newsletter_id) @@ -76,7 +76,7 @@ def delete_existing_newsletter(newsletter_id: int, db: Session = Depends(get_db) @router.post("/newsletters/{newsletter_id}/entries", response_model=Entry) def create_newsletter_entry( - newsletter_id: int, entry: EntryCreate, db: Session = Depends(get_db) + newsletter_id: str, entry: EntryCreate, db: Session = Depends(get_db) ): """Create a new entry for a specific newsletter.""" logger.info(f"Request to create entry for newsletter_id={newsletter_id}") diff --git a/backend/app/schemas/entries.py b/backend/app/schemas/entries.py index 3c46f93..ea5afb1 100644 --- a/backend/app/schemas/entries.py +++ b/backend/app/schemas/entries.py @@ -20,8 +20,8 @@ class EntryCreate(EntryBase): class Entry(EntryBase): """Schema for retrieving an entry with its ID and newsletter ID.""" - id: int - newsletter_id: int + id: str + newsletter_id: str received_at: datetime.datetime model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/newsletters.py b/backend/app/schemas/newsletters.py index 25f4d2d..1293eda 100644 --- a/backend/app/schemas/newsletters.py +++ b/backend/app/schemas/newsletters.py @@ -18,8 +18,8 @@ class SenderCreate(SenderBase): class Sender(SenderBase): """Schema for retrieving a sender with its ID and newsletter ID.""" - id: int - newsletter_id: int + id: str + newsletter_id: str model_config = ConfigDict(from_attributes=True) @@ -47,7 +47,7 @@ class NewsletterUpdate(NewsletterBase): class Newsletter(NewsletterBase): """Schema for retrieving a newsletter with its ID, active status, senders, and entries count.""" - id: int + id: str is_active: bool senders: List[Sender] = [] entries_count: int diff --git a/backend/app/services/feed_generator.py b/backend/app/services/feed_generator.py index 33c4713..8f9e785 100644 --- a/backend/app/services/feed_generator.py +++ b/backend/app/services/feed_generator.py @@ -7,7 +7,7 @@ from app.crud.entries import get_entries_by_newsletter from app.crud.newsletters import get_newsletter -def generate_feed(db: Session, newsletter_id: int): +def generate_feed(db: Session, newsletter_id: str): """Generate an Atom feed for a given newsletter.""" newsletter = get_newsletter(db, newsletter_id) if not newsletter: diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 0ca49c7..78c70ca 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "apscheduler>=3.11.0", "fastapi>=0.116.0", "feedgen>=1.0.0", + "nanoid>=2.0.0", "pydantic-settings>=2.10.1", "python-dotenv>=1.1.1", "sqlalchemy>=2.0.41", diff --git a/backend/uv.lock b/backend/uv.lock index b0356d3..992351f 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -324,6 +324,7 @@ dependencies = [ { name = "apscheduler" }, { name = "fastapi" }, { name = "feedgen" }, + { name = "nanoid" }, { name = "pydantic-settings" }, { name = "python-dotenv" }, { name = "sqlalchemy" }, @@ -344,6 +345,7 @@ requires-dist = [ { name = "apscheduler", specifier = ">=3.11.0" }, { name = "fastapi", specifier = ">=0.116.0" }, { name = "feedgen", specifier = ">=1.0.0" }, + { name = "nanoid", specifier = ">=2.0.0" }, { name = "pydantic-settings", specifier = ">=2.10.1" }, { name = "python-dotenv", specifier = ">=1.1.1" }, { name = "sqlalchemy", specifier = ">=2.0.41" }, @@ -418,6 +420,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/0b/942cb7278d6caad79343ad2ddd636ed204a47909b969d19114a3097f5aa3/lxml_html_clean-0.4.2-py3-none-any.whl", hash = "sha256:74ccfba277adcfea87a1e9294f47dd86b05d65b4da7c5b07966e3d5f3be8a505", size = 14184, upload-time = "2025-04-09T11:33:57.988Z" }, ] +[[package]] +name = "nanoid" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/0250bf5935d88e214df469d35eccc0f6ff7e9db046fc8a9aeb4b2a192775/nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68", size = 3290, upload-time = "2018-11-20T14:45:51.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/0d/8630f13998638dc01e187fadd2e5c6d42d127d08aeb4943d231664d6e539/nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb", size = 5844, upload-time = "2018-11-20T14:45:50.165Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index c205fc6..6b609ae 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,18 +1,18 @@ const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL; export interface Sender { - id: number; + id: string; email: string; - newsletter_id: number; + newsletter_id: string; } export interface Newsletter { - id: number + id: string name: string is_active: boolean move_to_folder?: string | null extract_content: boolean - senders: { id: number; email: string }[] + senders: { id: string; email: string }[] entries_count: number } @@ -106,7 +106,7 @@ export async function createNewsletter(newsletter: NewsletterCreate): Promise { +export async function updateNewsletter(id: string, newsletter: NewsletterUpdate): Promise { return fetcher(`${API_BASE_URL}/newsletters/${id}`, { method: 'PUT', headers: { @@ -116,7 +116,7 @@ export async function updateNewsletter(id: number, newsletter: NewsletterUpdate) }, "Failed to update newsletter"); } -export async function deleteNewsletter(id: number): Promise { +export async function deleteNewsletter(id: string): Promise { await fetcher(`${API_BASE_URL}/newsletters/${id}`, { method: 'DELETE', }, "Failed to delete newsletter"); @@ -152,6 +152,6 @@ export async function processEmails(): Promise<{ message: string }> { }, "Failed to process emails"); } -export function getFeedUrl(newsletterId: number): string { +export function getFeedUrl(newsletterId: string): string { return `${API_BASE_URL}/feeds/${newsletterId}`; }