Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
cb1306a
chore(git): ignore PostgreSQL data directory
maltonoloco Mar 2, 2026
a022aa1
fix(database): preserve FastAPI exception status codes
maltonoloco Mar 2, 2026
79912a7
fix(database): remove incompatible QueuePool from async engine
maltonoloco Mar 2, 2026
569dd89
feat(app): auto-create database tables on startup
maltonoloco Mar 2, 2026
2919536
feat(services): add complete CRUD API for item management
maltonoloco Mar 2, 2026
08abd31
test(crud-item-store): add 75 tests with 100% coverage
maltonoloco Mar 2, 2026
94b7020
docs(crud-item-store): add service documentation
maltonoloco Mar 2, 2026
4e6bbcd
chore(docker): change API port from 8000 to 8001
maltonoloco Mar 2, 2026
40cfdea
chore(config): update database URL in environment example
maltonoloco Mar 2, 2026
28130c9
fix(crud_item_store): run linter
maltonoloco Mar 2, 2026
6db95d8
refactor(crud_item_store): move transformation logic to functions module
maltonoloco Mar 3, 2026
06ffe95
fix(tests): correct API base URL path in integration tests
maltonoloco Mar 3, 2026
78244d5
feat(crud-item-store): add generic field duplication validation function
maltonoloco Mar 3, 2026
da518d0
refactor(crud-item-store): replace HTTPException with shared exceptio…
maltonoloco Mar 3, 2026
0461904
feat(api): add global exception handler for AppException
maltonoloco Mar 3, 2026
5305b83
fix(database): allow AppException to pass through session context man…
maltonoloco Mar 3, 2026
5771edf
test(crud-item-store): update integration tests for new error respons…
maltonoloco Mar 3, 2026
9db021f
refactor(crud-item-store): consolidate search methods and add generic…
maltonoloco Mar 3, 2026
d44a33a
refactor(crud-item-store): simplify check_duplicate_field to use gene…
maltonoloco Mar 3, 2026
ab12990
refactor(crud-item-store): remove redundant sku_exists and slug_exist…
maltonoloco Mar 3, 2026
d613613
refactor(crud_item_store): move ItemResponse to responses directory
maltonoloco Mar 3, 2026
c88773e
refactor(crud_item_store): use shared PaginatedResponse in list endpoint
maltonoloco Mar 3, 2026
0e12e43
test(crud_item_store): update unit tests for new response structure
maltonoloco Mar 3, 2026
ed0a3ea
test(crud_item_store): update integration tests for PaginatedResponse
maltonoloco Mar 3, 2026
54aa146
refactor(app): move lifespan to chore module
maltonoloco Mar 3, 2026
827b4c3
fix(crud_item_store): run linter
maltonoloco Mar 3, 2026
a175aa1
docs(crud_item_store): condense documentation by removing verbose exa…
maltonoloco Mar 4, 2026
584f83c
refactor(models): use Pydantic Annotated types for field validation
maltonoloco Mar 4, 2026
9c98608
feat(functions): add validate_update_conflicts for item updates
maltonoloco Mar 4, 2026
439c275
feat(functions): add prepare_item_update_data for DB conversion
maltonoloco Mar 4, 2026
11bc494
chore(functions): export new validation and transformation functions
maltonoloco Mar 4, 2026
a260f13
refactor(routers): use helper functions in update_item endpoint
maltonoloco Mar 4, 2026
0f03ffe
refactor: simplify imports using package-level exports
maltonoloco Mar 4, 2026
d5beab2
refactor(docs): extract OpenAPI documentation to separate module
maltonoloco Mar 4, 2026
0e5381e
feat(api): add catch-all exception handler for unexpected errors
maltonoloco Mar 4, 2026
b9ab9c3
refactor(repository): remove redundant get_by_sku and get_by_slug met…
maltonoloco Mar 4, 2026
2b900de
fix(crud_item_store): run linter
maltonoloco Mar 4, 2026
bc24fac
docs: add 500 error examples to OpenAPI documentation
maltonoloco Mar 4, 2026
fdaff12
refactor: change get_by_slug endpoint to get_by_sku
maltonoloco Mar 4, 2026
b817fbd
test: update integration test for get_by_sku endpoint
maltonoloco Mar 4, 2026
5118157
docs: add 500 error examples to list and get_by_sku endpoints
maltonoloco Mar 4, 2026
668cc41
docs: add missing PaginatedResponse model to list endpoint
maltonoloco Mar 4, 2026
345cca7
fix(crud_item_store): run linter
maltonoloco Mar 4, 2026
ca09e90
fix(routers): use repo.filter() instead of repo.get_all() in list_items
maltonoloco Mar 4, 2026
0acff39
refactor(transformations): replace manual field mapping with model_va…
maltonoloco Mar 4, 2026
373208c
fix(responses): use lowercase error_code/error_category in Validation…
maltonoloco Mar 4, 2026
964b1ed
fix(main): handle RequestValidationError with structured ValidationEr…
maltonoloco Mar 4, 2026
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ RELOAD=false
# ----------------------------------
# PostgreSQL connection URL
# Format: postgresql+asyncpg://user:password@host:port/database
DATABASE_URL=postgresql+asyncpg://opentaberna:opentaberna@localhost:5432/opentaberna
DATABASE_URL=postgresql+asyncpg://opentaberna:opentaberna_password@opentaberna-db:5432/opentaberna

# Connection pool settings
DATABASE_POOL_SIZE=20
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,7 @@ cython_debug/
# PyCharm
# .idea/

.env.test
.env.test

# postgresql db directory
opentaberna-postgres
6 changes: 3 additions & 3 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ services:
context: .
dockerfile: src/Dockerfile
image: opentaberna-api:latest
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
command: uvicorn app.main:app --host 0.0.0.0 --port 8001
env_file: .env
expose:
- "8000"
ports:
- "8001:8001"
networks:
frontproxy_fnet:
ipv4_address: 172.20.20.21
Expand Down
235 changes: 235 additions & 0 deletions docs/crud_item_store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# CRUD Item Store Service Documentation

## Overview

The **crud-item-store** service provides CRUD operations for managing store items with self-contained models, business logic, and API endpoints.

## Architecture

```
src/app/services/crud-item-store/
├── __init__.py # Service entry point & router exports
├── models/
│ ├── __init__.py # Model exports
│ ├── item.py # Pydantic data models (ItemBase, ItemCreate, ItemUpdate)
│ └── database.py # SQLAlchemy ORM model
├── responses/
│ ├── __init__.py # Response model exports
│ └── items.py # API response schemas (ItemResponse)
├── routers/
│ ├── __init__.py # Router exports
│ └── items.py # CRUD API endpoints
├── services/
│ ├── __init__.py # Service exports
│ └── database.py # Database repository layer
└── functions/
├── __init__.py # Function exports
├── transformations.py # Data transformation functions
└── validation.py # Validation functions
```

---

## Pydantic Models

### Data Models (`models/item.py`)

#### `ItemBase`
Base schema with all core item fields (shared by Create, Update, Response).

#### `ItemCreate`
Schema for creating new items. All fields from `ItemBase` are required except those with defaults.

#### `ItemUpdate`
Schema for updating items. All fields are optional - only provided fields will be updated.

### Response Models (`responses/items.py`)

#### `ItemResponse`
API response schema including `uuid`, `created_at`, and `updated_at` timestamps. Extends `ItemBase` with database-generated fields.

**Note:** List endpoints use shared `PaginatedResponse[ItemResponse]` with `success`, `items`, `page_info`, `message`, and `timestamp` fields.

### Nested Models

`PriceModel`, `MediaModel`, `InventoryModel`, `ShippingModel`, `WeightModel`, `DimensionsModel`, `IdentifiersModel`, `SystemModel`

### Enums

`ItemStatus`, `StockStatus`, `TaxClass`, `ShippingClass`, `WeightUnit`, `DimensionUnit`

---

## Database Model

### `ItemDB` (SQLAlchemy)

Stored in PostgreSQL with optimized structure:

**Columns** (indexed for queries):
- `uuid`: Primary key (UUID)
- `sku`: Unique stock keeping unit (indexed)
- `status`: Item status (indexed)
- `name`: Display name (indexed)
- `slug`: URL-friendly identifier (unique, indexed)
- `short_description`: Brief text
- `description`: Full text/HTML
- `brand`: Brand name (indexed)

**JSONB Fields** (for complex nested data):
- `categories`: Array of category UUIDs
- `price`: Price information object
- `media`: Media assets object
- `inventory`: Inventory data object
- `shipping`: Shipping information object
- `attributes`: Custom key-value pairs
- `identifiers`: Product codes object
- `custom`: Extensible plugin data
- `system`: System metadata

**Timestamps**: `created_at`, `updated_at` (auto-managed via `TimestampMixin`)

---

## Repository Layer

### `ItemRepository` (`services/database.py`)

Extends `BaseRepository[ItemDB]` with item-specific methods:

#### Basic CRUD (inherited from BaseRepository)
- `create(**fields)`: Create new item
- `get(uuid)`: Get by UUID
- `update(uuid, **fields)`: Update item
- `delete(uuid)`: Delete item
- `get_all(skip, limit, **filters)`: List with pagination
- `count(**filters)`: Count items
- `get_by(**filters)`: Get single item by field(s)

#### Item-Specific Queries
- `get_by_sku(sku)`: Find by SKU
- `get_by_slug(slug)`: Find by URL slug
- `search(name, status, category_uuid, brand, skip, limit)`: Generic search with multiple optional criteria (AND logic)
- `field_exists(field_name, field_value, exclude_uuid)`: Generic field existence check

---

## API Endpoints

Base path: `/v1/items`

- `POST /items/` - Create item (201 Created)
- `GET /items/{uuid}` - Get by UUID (200 OK / 404 Not Found)
- `GET /items/by-slug/{slug}` - Get by slug (200 OK / 404 Not Found)
- `GET /items/?skip=0&limit=50&status=active` - List with pagination (200 OK)
- `PATCH /items/{uuid}` - Update item (200 OK / 404 Not Found)
- `DELETE /items/{uuid}` - Delete item (204 No Content / 404 Not Found)

**Validations:** SKU and slug uniqueness, currency codes (3 chars), non-negative amounts



---

## Integration

### Shared Module Integration

- **Exceptions**: `entity_not_found()`, `duplicate_entry()` → standardized error responses
- **Responses**: `PaginatedResponse[ItemResponse]`, `ErrorResponse` → consistent API format
- **Database**: `get_session_dependency`, `BaseRepository` → session management and base CRUD

### Register in Main App

In `src/app/main.py`:

```python
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from app.chore import lifespan
from app.services.crud_item_store import router as item_store_router
from app.shared.exceptions import AppException
from app.shared.responses import ErrorResponse

app = FastAPI(title="OpenTaberna API", lifespan=lifespan)

# Global exception handler for standardized error responses
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
error_response = ErrorResponse.from_exception(exc)
return JSONResponse(
status_code=error_response.status_code,
content=error_response.model_dump(mode="json")
)

# Include the item store router
app.include_router(item_store_router, prefix="/v1")
```

### Create Database Migration

```bash
alembic revision --autogenerate -m "create_items_table"
alembic upgrade head
```

---

## Validation

**Automatic (Pydantic):** SKU/slug uniqueness, currency codes, non-negative amounts, URL formats

**Business Logic (`functions/validation.py`):**
- `check_duplicate_field(repo, field_name, field_value, exclude_uuid)` - Generic uniqueness validation for any model field

---

## Functions Layer

**Transformations (`functions/transformations.py`):**
- `db_to_response(item_db)` - Converts SQLAlchemy models to Pydantic responses

**Validation (`functions/validation.py`):**
- `check_duplicate_field(repo, field_name, value, exclude_uuid)` - Generic uniqueness check



---

## Performance

**Indexes:** `uuid` (PK), `sku` (unique), `slug` (unique), `status`, `name`, `brand`

**Recommended GIN indexes for JSONB:** `price`, `categories`, `attributes`

**Pagination:** Max limit 100, default 50

---

## Error Handling

**Status Codes:** 200 (OK), 201 (Created), 204 (No Content), 404 (Not Found), 422 (Validation), 500 (Server Error)

**Format:** Standardized `ErrorResponse` with `success`, `error` (code, message, category), and `timestamp`

**Helpers:** `entity_not_found()` → 404, `duplicate_entry()` → 422



---

## Summary

**Features:**
- Complete CRUD operations with PostgreSQL JSONB storage
- Type-safe Pydantic models with nested structures
- Repository pattern with generic search and validation
- Shared exception/response system integration
- Pagination, error handling, and proper indexing

**Architecture:**
- `models/` - Data models (Pydantic)
- `responses/` - API responses
- `functions/` - Business logic
- `services/` - Database operations
- `routers/` - HTTP endpoints
10 changes: 10 additions & 0 deletions src/app/chore/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Chore Module

Infrastructure and operational tasks such as application lifecycle management,
database initialization, health checks, and scheduled maintenance tasks.
"""

from .lifespan import lifespan

__all__ = ["lifespan"]
36 changes: 36 additions & 0 deletions src/app/chore/lifespan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Application Lifespan Management

Handles startup and shutdown events for the FastAPI application,
including database initialization and cleanup.
"""

from contextlib import asynccontextmanager

from fastapi import FastAPI

from app.shared.database.base import Base
from app.shared.database.engine import close_database, get_engine, init_database


@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Application lifespan events.

Startup:
- Initialize database connection pool
- Create all tables from SQLAlchemy models

Shutdown:
- Close database connections gracefully
"""
# Startup: Initialize database and create tables
await init_database()
engine = get_engine()
async with engine.begin() as conn:
# This creates all tables from SQLAlchemy models that inherit from Base
await conn.run_sync(Base.metadata.create_all)
yield
# Shutdown
await close_database()
Loading
Loading