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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Add categories and products tables"""

from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes


revision = "78c83f2a0c24"
down_revision = "1a31ce608336"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"category",
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("id", sa.UUID(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"product",
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("price", sa.Float(), nullable=False),
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("category_id", sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(["category_id"], ["category.id"], ondelete="RESTRICT"),
sa.PrimaryKeyConstraint("id"),
)


def downgrade() -> None:
op.drop_table("product")
op.drop_table("category")
12 changes: 11 additions & 1 deletion backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
from fastapi import APIRouter

from app.api.routes import items, login, private, users, utils
from app.api.routes import (
categories,
items,
login,
private,
products,
users,
utils,
)
from app.core.config import settings

api_router = APIRouter()
api_router.include_router(login.router)
api_router.include_router(users.router)
api_router.include_router(utils.router)
api_router.include_router(items.router)
api_router.include_router(categories.router)
api_router.include_router(products.router)


if settings.ENVIRONMENT == "local":
Expand Down
97 changes: 97 additions & 0 deletions backend/app/api/routes/categories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import uuid
from typing import Any

from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import func, select

from app.api.deps import SessionDep, get_current_active_superuser
from app.models import (
CategoriesPublic,
Category,
CategoryCreate,
CategoryPublic,
CategoryUpdate,
Message,
Product,
)

router = APIRouter(prefix="/categories", tags=["categories"])


@router.get("/", response_model=CategoriesPublic)
def read_categories(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
"""Retrieve categories."""

count_statement = select(func.count()).select_from(Category)
count = session.exec(count_statement).one()
statement = select(Category).offset(skip).limit(limit)
categories = session.exec(statement).all()
return CategoriesPublic(data=categories, count=count)


@router.get("/{category_id}", response_model=CategoryPublic)
def read_category(category_id: uuid.UUID, session: SessionDep) -> Any:
"""Get category by ID."""

category = session.get(Category, category_id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
return category


@router.post(
"/",
dependencies=[Depends(get_current_active_superuser)],
response_model=CategoryPublic,
)
def create_category(*, session: SessionDep, category_in: CategoryCreate) -> Any:
"""Create new category."""

category = Category.model_validate(category_in)
session.add(category)
session.commit()
session.refresh(category)
return category


@router.put(
"/{category_id}",
dependencies=[Depends(get_current_active_superuser)],
response_model=CategoryPublic,
)
def update_category(
*, session: SessionDep, category_id: uuid.UUID, category_in: CategoryUpdate
) -> Any:
"""Update a category."""

category = session.get(Category, category_id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
update_data = category_in.model_dump(exclude_unset=True)
category.sqlmodel_update(update_data)
session.add(category)
session.commit()
session.refresh(category)
return category


@router.delete("/{category_id}", dependencies=[Depends(get_current_active_superuser)])
def delete_category(session: SessionDep, category_id: uuid.UUID) -> Message:
"""Delete a category without products."""

category = session.get(Category, category_id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
count_products = session.exec(
select(func.count())
.select_from(Product)
.where(Product.category_id == category_id)
).one()
if count_products:
raise HTTPException(
status_code=400,
detail="Cannot delete category with existing products",
)
session.delete(category)
session.commit()
return Message(message="Category deleted successfully")
94 changes: 94 additions & 0 deletions backend/app/api/routes/products.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import uuid
from typing import Any

from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import func, select

from app.api.deps import SessionDep, get_current_active_superuser
from app.models import (
Category,
Message,
Product,
ProductCreate,
ProductPublic,
ProductsPublic,
ProductUpdate,
)

router = APIRouter(prefix="/products", tags=["products"])


@router.get("/", response_model=ProductsPublic)
def read_products(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
"""Retrieve products."""

count_statement = select(func.count()).select_from(Product)
count = session.exec(count_statement).one()
statement = select(Product).offset(skip).limit(limit)
products = session.exec(statement).all()
return ProductsPublic(data=products, count=count)


@router.get("/{product_id}", response_model=ProductPublic)
def read_product(product_id: uuid.UUID, session: SessionDep) -> Any:
"""Get product by ID."""

product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product


@router.post(
"/",
dependencies=[Depends(get_current_active_superuser)],
response_model=ProductPublic,
)
def create_product(*, session: SessionDep, product_in: ProductCreate) -> Any:
"""Create new product."""

category = session.get(Category, product_in.category_id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
product = Product.model_validate(product_in)
session.add(product)
session.commit()
session.refresh(product)
return product


@router.put(
"/{product_id}",
dependencies=[Depends(get_current_active_superuser)],
response_model=ProductPublic,
)
def update_product(
*, session: SessionDep, product_id: uuid.UUID, product_in: ProductUpdate
) -> Any:
"""Update a product."""

product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
if product_in.category_id:
category = session.get(Category, product_in.category_id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
update_data = product_in.model_dump(exclude_unset=True)
product.sqlmodel_update(update_data)
session.add(product)
session.commit()
session.refresh(product)
return product


@router.delete("/{product_id}", dependencies=[Depends(get_current_active_superuser)])
def delete_product(session: SessionDep, product_id: uuid.UUID) -> Message:
"""Delete a product."""

product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
session.delete(product)
session.commit()
return Message(message="Product deleted successfully")
28 changes: 27 additions & 1 deletion backend/app/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@
from sqlmodel import Session, select

from app.core.security import get_password_hash, verify_password
from app.models import Item, ItemCreate, User, UserCreate, UserUpdate
from app.models import (
Category,
CategoryCreate,
Item,
ItemCreate,
Product,
ProductCreate,
User,
UserCreate,
UserUpdate,
)


def create_user(*, session: Session, user_create: UserCreate) -> User:
Expand Down Expand Up @@ -52,3 +62,19 @@ def create_item(*, session: Session, item_in: ItemCreate, owner_id: uuid.UUID) -
session.commit()
session.refresh(db_item)
return db_item


def create_category(*, session: Session, category_in: CategoryCreate) -> Category:
db_category = Category.model_validate(category_in)
session.add(db_category)
session.commit()
session.refresh(db_category)
return db_category


def create_product(*, session: Session, product_in: ProductCreate) -> Product:
db_product = Product.model_validate(product_in)
session.add(db_product)
session.commit()
session.refresh(db_product)
return db_product
63 changes: 63 additions & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,69 @@ class ItemsPublic(SQLModel):
count: int


# Shared properties
class CategoryBase(SQLModel):
name: str = Field(min_length=1, max_length=255)
description: str | None = Field(default=None, max_length=255)


class CategoryCreate(CategoryBase):
pass


class CategoryUpdate(CategoryBase):
name: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore


class Category(CategoryBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
products: list["Product"] = Relationship(back_populates="category")


class CategoryPublic(CategoryBase):
id: uuid.UUID


class CategoriesPublic(SQLModel):
data: list[CategoryPublic]
count: int


# Shared properties
class ProductBase(SQLModel):
name: str = Field(min_length=1, max_length=255)
description: str | None = Field(default=None, max_length=255)
price: float


class ProductCreate(ProductBase):
category_id: uuid.UUID


class ProductUpdate(ProductBase):
name: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
price: float | None = None
category_id: uuid.UUID | None = None


class Product(ProductBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
category_id: uuid.UUID = Field(
foreign_key="category.id", nullable=False, ondelete="RESTRICT"
)
category: Category | None = Relationship(back_populates="products")


class ProductPublic(ProductBase):
id: uuid.UUID
category_id: uuid.UUID


class ProductsPublic(SQLModel):
data: list[ProductPublic]
count: int


# Generic message
class Message(SQLModel):
message: str
Expand Down
Loading