Files
orion/docs/architecture/models-structure.md
Samir Boulahtit 1ad30bd77e docs: update models-structure with hybrid architecture patterns
Document the current architecture for models and schemas:
- CORE models (models/database/) vs module models (app/modules/*/models/)
- Three schema patterns: dedicated files, inline, and no-schema
- Data flow diagram showing services access DB directly
- Gap analysis table showing model-schema alignment
- Import guidelines with canonical vs legacy re-exports
- Checklists for creating new models and schemas

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 20:09:16 +01:00

328 lines
14 KiB
Markdown

# Models & Schemas Architecture
## Overview
This project uses a **hybrid architecture** for models and schemas:
1. **CORE models/schemas** in `models/database/` and `models/schema/` - Framework-level entities used across modules
2. **Module models/schemas** in `app/modules/<module>/models/` and `app/modules/<module>/schemas/` - Domain-specific entities
3. **Inline schemas** defined directly in API route files - Endpoint-specific request/response models
## Directory Structure
```
models/ # CORE framework models
├── database/ # SQLAlchemy models (ORM)
│ ├── __init__.py # Exports all models + module discovery
│ ├── base.py # Base class, mixins
│ ├── user.py # User authentication
│ ├── vendor.py # Vendor, VendorUser, Role
│ ├── company.py # Company management
│ ├── platform.py # Multi-platform support
│ ├── media.py # MediaFile (cross-cutting)
│ └── ...
└── schema/ # Pydantic schemas (API validation)
├── __init__.py
├── auth.py # Login, tokens, password reset
├── vendor.py # Vendor CRUD schemas
├── company.py # Company schemas
└── ...
app/modules/<module>/ # Domain modules
├── models/ # Module-specific database models
│ ├── __init__.py # Canonical exports
│ └── *.py # Model definitions
└── schemas/ # Module-specific schemas
├── __init__.py # Canonical exports
└── *.py # Schema definitions
```
---
## Architecture Principles
### 1. CORE vs Module Models
**CORE models** (`models/database/`) are framework-level entities used across multiple modules:
- `User` - Authentication, used by all modules
- `Vendor` - Multi-tenant anchor, used by all vendor-scoped modules
- `Company` - Business entity management
- `Platform` - Multi-platform CMS support
- `MediaFile` - File storage, used by catalog, CMS, etc.
**Module models** (`app/modules/<module>/models/`) are domain-specific:
- `billing/models/` - Feature, SubscriptionTier, VendorSubscription
- `catalog/models/` - Product, ProductTranslation, ProductMedia
- `orders/models/` - Order, OrderItem, Invoice
- `inventory/models/` - Inventory, InventoryTransaction
### 2. Schema Patterns
The codebase uses **three valid patterns** for schemas:
#### Pattern A: Dedicated Schema Files (Reusable)
For schemas used across multiple endpoints or modules.
```python
# app/modules/inventory/schemas/inventory.py
class InventoryCreate(BaseModel):
product_id: int
location: str
quantity: int
class InventoryResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
product_id: int
vendor_id: int
quantity: int
```
#### Pattern B: Inline Schemas (Endpoint-Specific)
For schemas only used by a single endpoint or closely related endpoints.
```python
# app/api/v1/admin/platforms.py
class PlatformResponse(BaseModel):
"""Platform response schema - only used in this file."""
id: int
code: str
name: str
# ...
class PlatformUpdateRequest(BaseModel):
"""Request schema - only used in this file."""
name: str | None = None
description: str | None = None
@router.get("/platforms/{code}", response_model=PlatformResponse)
def get_platform(...):
...
```
#### Pattern C: No Schema (Internal Service Use)
For models only accessed internally by services, not exposed via API.
```python
# Internal association table - no API exposure
class VendorPlatform(Base):
"""Links vendors to platforms - internal use only."""
__tablename__ = "vendor_platforms"
vendor_id = Column(Integer, ForeignKey("vendors.id"))
platform_id = Column(Integer, ForeignKey("platforms.id"))
```
### 3. When to Use Each Pattern
| Scenario | Pattern | Location |
|----------|---------|----------|
| Schema used by multiple endpoints | Dedicated file | `app/modules/<module>/schemas/` |
| Schema used by single endpoint | Inline | In the route file |
| Admin-only endpoint schemas | Inline | In the admin route file |
| Model not exposed via API | No schema | N/A |
| Cross-module utility schemas | Dedicated file | `models/schema/` |
---
## Data Flow Architecture
```
┌─────────────────────────────────────────────────────────────────────┐
│ API Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Request Schema │───▶│ Endpoint │───▶│ Response Schema │ │
│ │ (Pydantic) │ │ │ │ (Pydantic) │ │
│ └─────────────────┘ └────────┬────────┘ └─────────────────┘ │
└──────────────────────────────────┼──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Service Layer │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Business Logic │ │
│ │ • Validation • Transformations • Business Rules │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Database Models (SQLAlchemy) │ │
│ │ • Direct access • Queries • Transactions │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Database Layer │
│ PostgreSQL / SQLite │
└─────────────────────────────────────────────────────────────────────┘
```
**Key principles:**
- **Services access database models directly** - No schema layer between service and database
- **APIs use schemas for validation** - Request/response validation at API boundary
- **Schemas are API contracts** - Define what clients send/receive
---
## Import Guidelines
### Canonical Imports (Preferred)
```python
# CORE models - from models.database
from models.database import User, Vendor, MediaFile
# Module models - from app.modules.<module>.models
from app.modules.billing.models import Feature, SubscriptionTier
from app.modules.catalog.models import Product, ProductMedia
from app.modules.orders.models import Order, OrderItem
# CORE schemas - from models.schema
from models.schema.auth import LoginRequest, TokenResponse
# Module schemas - from app.modules.<module>.schemas
from app.modules.inventory.schemas import InventoryCreate, InventoryResponse
from app.modules.orders.schemas import OrderResponse
```
### Legacy Re-exports (Backwards Compatibility)
`models/database/__init__.py` re-exports module models for backwards compatibility:
```python
# These work but prefer canonical imports
from models.database import Product # Re-exported from catalog module
from models.database import Order # Re-exported from orders module
```
---
## Gap Analysis: Models vs Schemas
Not every database model needs a dedicated schema file. Here's the current alignment:
### CORE Framework
| Database Model | Schema | Notes |
|----------------|--------|-------|
| `user.py` | `auth.py` | Auth schemas cover user operations |
| `vendor.py` | `vendor.py` | Full CRUD schemas |
| `company.py` | `company.py` | Full CRUD schemas |
| `media.py` | `media.py` | Upload/response schemas |
| `platform.py` | Inline | Admin-only, in `platforms.py` route |
| `platform_module.py` | Inline | Admin-only, in `modules.py` route |
| `admin_menu_config.py` | Inline | Admin-only, in `menu_config.py` route |
| `vendor_email_settings.py` | Inline | In `email_settings.py` route |
| `vendor_email_template.py` | None | Internal email service use |
| `vendor_platform.py` | None | Internal association table |
### Modules
| Module | Models | Schemas | Alignment |
|--------|--------|---------|-----------|
| billing | feature, subscription | billing, subscription | ✅ |
| cart | cart | cart | ✅ |
| catalog | product, product_media, product_translation | catalog, product, vendor_product | ✅ |
| checkout | - | checkout | Schema-only (orchestration) |
| cms | content_page | content_page, homepage_sections | ✅ |
| customers | customer, password_reset_token | customer, context | ✅ (password reset uses auth schemas) |
| dev_tools | architecture_scan, test_run | Inline | Admin-only, inline in route files |
| inventory | inventory, inventory_transaction | inventory | ✅ (transaction included in inventory.py) |
| loyalty | 5 models | 5 schemas | ✅ |
| marketplace | 5 models | 4 schemas | ✅ (translation in product schema) |
| messaging | admin_notification, message | message, notification | ✅ |
| orders | order, order_item, invoice | order, invoice, order_item_exception | ✅ |
| payments | - | payment | Schema-only (external APIs) |
| analytics | - | stats | Schema-only (aggregation views) |
---
## Creating New Models
### Database Model Checklist
1. **Determine location:**
- Cross-module use → `models/database/`
- Module-specific → `app/modules/<module>/models/`
2. **Create the model file:**
```python
# app/modules/mymodule/models/my_entity.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from app.core.database import Base
from models.database.base import TimestampMixin
class MyEntity(Base, TimestampMixin):
__tablename__ = "my_entities"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
name = Column(String(255), nullable=False)
vendor = relationship("Vendor")
```
3. **Export in `__init__.py`:**
```python
# app/modules/mymodule/models/__init__.py
from app.modules.mymodule.models.my_entity import MyEntity
__all__ = ["MyEntity"]
```
### Schema Checklist
1. **Decide if schema is needed:**
- Exposed via API? → Yes, create schema
- Multiple endpoints use it? → Dedicated schema file
- Single endpoint? → Consider inline schema
- Internal only? → No schema needed
2. **Create schema (if needed):**
```python
# app/modules/mymodule/schemas/my_entity.py
from pydantic import BaseModel, ConfigDict, Field
class MyEntityCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
class MyEntityResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
vendor_id: int
name: str
```
3. **Or use inline schema:**
```python
# app/api/v1/vendor/my_entity.py
from pydantic import BaseModel
class MyEntityResponse(BaseModel):
"""Response schema - only used in this file."""
id: int
name: str
@router.get("/my-entities/{id}", response_model=MyEntityResponse)
def get_entity(...):
...
```
---
## Summary
| Principle | Implementation |
|-----------|----------------|
| Services access DB directly | `from app.modules.X.models import Model` |
| APIs validate with schemas | Request/response Pydantic models |
| Reusable schemas | Dedicated files in `schemas/` |
| Endpoint-specific schemas | Inline in route files |
| Internal models | No schema needed |
| CORE models | `models/database/` |
| Module models | `app/modules/<module>/models/` |