Initial commit
This commit is contained in:
66
skills/data-validation/templates/INDEX.md
Normal file
66
skills/data-validation/templates/INDEX.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Data Validation Templates
|
||||
|
||||
Copy-paste templates for common data validation patterns.
|
||||
|
||||
## Available Templates
|
||||
|
||||
### Pydantic Model Template
|
||||
**File**: [pydantic-model.py](pydantic-model.py)
|
||||
|
||||
Complete Pydantic v2 model template with:
|
||||
- Field definitions with constraints
|
||||
- Custom validators (@field_validator, @model_validator)
|
||||
- model_config configuration
|
||||
- Nested models
|
||||
- Documentation
|
||||
|
||||
**Use when**: Starting a new API request/response schema.
|
||||
|
||||
---
|
||||
|
||||
### SQLModel Template
|
||||
**File**: [sqlmodel-model.py](sqlmodel-model.py)
|
||||
|
||||
Complete SQLModel database template with:
|
||||
- Table configuration
|
||||
- Field definitions with PostgreSQL types
|
||||
- Multi-tenant pattern (tenant_id)
|
||||
- Timestamps (created_at, updated_at)
|
||||
- Indexes and constraints
|
||||
- Relationships
|
||||
|
||||
**Use when**: Creating a new database table.
|
||||
|
||||
---
|
||||
|
||||
### FastAPI Endpoint Template
|
||||
**File**: [fastapi-endpoint.py](fastapi-endpoint.py)
|
||||
|
||||
Complete FastAPI endpoint template with:
|
||||
- Router configuration
|
||||
- Pydantic request/response schemas
|
||||
- Dependency injection (session, tenant_id, user_id)
|
||||
- Validation error handling
|
||||
- Database operations
|
||||
- Multi-tenant isolation
|
||||
|
||||
**Use when**: Creating a new API endpoint with validation.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Copy template** to your project
|
||||
2. **Rename** model/endpoint appropriately
|
||||
3. **Customize** fields and validators
|
||||
4. **Test** with comprehensive test cases
|
||||
|
||||
## Navigation
|
||||
|
||||
- **Examples**: [Examples Index](../examples/INDEX.md)
|
||||
- **Reference**: [Reference Index](../reference/INDEX.md)
|
||||
- **Main Agent**: [data-validator.md](../data-validator.md)
|
||||
|
||||
---
|
||||
|
||||
Return to [main agent](../data-validator.md)
|
||||
311
skills/data-validation/templates/fastapi-endpoint.py
Normal file
311
skills/data-validation/templates/fastapi-endpoint.py
Normal file
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
FastAPI Endpoint Template
|
||||
|
||||
Copy this template to create new API endpoints with validation.
|
||||
Replace {ModelName}, {endpoint_prefix}, and {table_name} with your actual values.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, status
|
||||
from sqlmodel import Session, select
|
||||
from pydantic import ValidationError
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
# Import your models and schemas
|
||||
from app.models.{model_name} import {ModelName}
|
||||
from app.schemas.{model_name} import (
|
||||
{ModelName}CreateSchema,
|
||||
{ModelName}UpdateSchema,
|
||||
{ModelName}ResponseSchema
|
||||
)
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user, get_current_tenant_id
|
||||
|
||||
|
||||
# Create router
|
||||
# -------------
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/{endpoint_prefix}",
|
||||
tags=["{endpoint_prefix}"]
|
||||
)
|
||||
|
||||
|
||||
# Create Endpoint
|
||||
# --------------
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model={ModelName}ResponseSchema,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create new {ModelName}",
|
||||
description="Create a new {ModelName} with validation"
|
||||
)
|
||||
async def create_{model_name}(
|
||||
data: {ModelName}CreateSchema,
|
||||
session: Session = Depends(get_session),
|
||||
user_id: UUID = Depends(get_current_user),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
Create new {ModelName}.
|
||||
|
||||
Validates:
|
||||
- Request data against Pydantic schema
|
||||
- Business rules (if any)
|
||||
- Uniqueness constraints
|
||||
- Multi-tenant isolation
|
||||
|
||||
Returns:
|
||||
{ModelName}ResponseSchema: Created {ModelName}
|
||||
|
||||
Raises:
|
||||
HTTPException 409: If duplicate exists
|
||||
HTTPException 422: If validation fails
|
||||
"""
|
||||
|
||||
# Check for duplicates (if applicable)
|
||||
existing = session.exec(
|
||||
select({ModelName})
|
||||
.where({ModelName}.email == data.email)
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="{ ModelName} with this email already exists"
|
||||
)
|
||||
|
||||
# Create instance
|
||||
instance = {ModelName}(
|
||||
**data.model_dump(),
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
session.add(instance)
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
|
||||
return {ModelName}ResponseSchema.model_validate(instance)
|
||||
|
||||
|
||||
# Read Endpoints
|
||||
# -------------
|
||||
|
||||
@router.get(
|
||||
"/{id}",
|
||||
response_model={ModelName}ResponseSchema,
|
||||
summary="Get {ModelName} by ID"
|
||||
)
|
||||
async def get_{model_name}(
|
||||
id: UUID,
|
||||
session: Session = Depends(get_session),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
Get {ModelName} by ID with tenant isolation.
|
||||
|
||||
Returns:
|
||||
{ModelName}ResponseSchema: Found {ModelName}
|
||||
|
||||
Raises:
|
||||
HTTPException 404: If not found
|
||||
"""
|
||||
|
||||
instance = session.exec(
|
||||
select({ModelName})
|
||||
.where({ModelName}.id == id)
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
).first()
|
||||
|
||||
if not instance:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="{ModelName} not found"
|
||||
)
|
||||
|
||||
return {ModelName}ResponseSchema.model_validate(instance)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=List[{ModelName}ResponseSchema],
|
||||
summary="List {ModelName}s"
|
||||
)
|
||||
async def list_{model_name}s(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
List {ModelName}s with pagination and filtering.
|
||||
|
||||
Args:
|
||||
skip: Number of records to skip (default 0)
|
||||
limit: Maximum records to return (default 100, max 1000)
|
||||
status: Filter by status (optional)
|
||||
|
||||
Returns:
|
||||
List[{ModelName}ResponseSchema]: List of {ModelName}s
|
||||
"""
|
||||
|
||||
# Build query
|
||||
statement = (
|
||||
select({ModelName})
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
.offset(skip)
|
||||
.limit(min(limit, 1000))
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if status:
|
||||
statement = statement.where({ModelName}.status == status)
|
||||
|
||||
# Execute
|
||||
results = session.exec(statement).all()
|
||||
|
||||
return [
|
||||
{ModelName}ResponseSchema.model_validate(item)
|
||||
for item in results
|
||||
]
|
||||
|
||||
|
||||
# Update Endpoint
|
||||
# --------------
|
||||
|
||||
@router.patch(
|
||||
"/{id}",
|
||||
response_model={ModelName}ResponseSchema,
|
||||
summary="Update {ModelName}"
|
||||
)
|
||||
async def update_{model_name}(
|
||||
id: UUID,
|
||||
data: {ModelName}UpdateSchema,
|
||||
session: Session = Depends(get_session),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
Update {ModelName} with partial data.
|
||||
|
||||
Only provided fields are updated.
|
||||
|
||||
Returns:
|
||||
{ModelName}ResponseSchema: Updated {ModelName}
|
||||
|
||||
Raises:
|
||||
HTTPException 404: If not found
|
||||
HTTPException 422: If validation fails
|
||||
"""
|
||||
|
||||
# Get existing
|
||||
instance = session.exec(
|
||||
select({ModelName})
|
||||
.where({ModelName}.id == id)
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
).first()
|
||||
|
||||
if not instance:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="{ModelName} not found"
|
||||
)
|
||||
|
||||
# Update fields
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(instance, field, value)
|
||||
|
||||
# Update timestamp
|
||||
instance.updated_at = datetime.utcnow()
|
||||
|
||||
session.add(instance)
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
|
||||
return {ModelName}ResponseSchema.model_validate(instance)
|
||||
|
||||
|
||||
# Delete Endpoint
|
||||
# --------------
|
||||
|
||||
@router.delete(
|
||||
"/{id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete {ModelName}"
|
||||
)
|
||||
async def delete_{model_name}(
|
||||
id: UUID,
|
||||
session: Session = Depends(get_session),
|
||||
tenant_id: UUID = Depends(get_current_tenant_id)
|
||||
):
|
||||
"""
|
||||
Delete {ModelName}.
|
||||
|
||||
Soft delete by setting is_active = False.
|
||||
For hard delete, use session.delete() instead.
|
||||
|
||||
Raises:
|
||||
HTTPException 404: If not found
|
||||
"""
|
||||
|
||||
instance = session.exec(
|
||||
select({ModelName})
|
||||
.where({ModelName}.id == id)
|
||||
.where({ModelName}.tenant_id == tenant_id)
|
||||
).first()
|
||||
|
||||
if not instance:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="{ModelName} not found"
|
||||
)
|
||||
|
||||
# Soft delete
|
||||
instance.is_active = False
|
||||
instance.updated_at = datetime.utcnow()
|
||||
|
||||
session.add(instance)
|
||||
session.commit()
|
||||
|
||||
# For hard delete:
|
||||
# session.delete(instance)
|
||||
# session.commit()
|
||||
|
||||
|
||||
# Error Handling
|
||||
# -------------
|
||||
|
||||
@router.exception_handler(ValidationError)
|
||||
async def validation_exception_handler(request, exc: ValidationError):
|
||||
"""Handle Pydantic validation errors."""
|
||||
errors = {}
|
||||
|
||||
for error in exc.errors():
|
||||
field = '.'.join(str(loc) for loc in error['loc'])
|
||||
message = error['msg']
|
||||
|
||||
if field not in errors:
|
||||
errors[field] = []
|
||||
errors[field].append(message)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={
|
||||
'success': False,
|
||||
'error': 'validation_error',
|
||||
'message': 'Request validation failed',
|
||||
'errors': errors
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Register Router
|
||||
# --------------
|
||||
|
||||
# In your main FastAPI app:
|
||||
# from app.api.{endpoint_prefix} import router as {model_name}_router
|
||||
# app.include_router({model_name}_router)
|
||||
231
skills/data-validation/templates/pydantic-model.py
Normal file
231
skills/data-validation/templates/pydantic-model.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Pydantic Model Template
|
||||
|
||||
Copy this template to create new Pydantic validation models.
|
||||
Replace {ModelName}, {field_name}, and {FieldType} with your actual values.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator, EmailStr, HttpUrl
|
||||
from typing import Optional, List, Literal
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
from enum import Enum
|
||||
|
||||
|
||||
# Optional: Define enums for categorical fields
|
||||
class {ModelName}Status(str, Enum):
|
||||
"""Status enum for {ModelName}."""
|
||||
ACTIVE = 'active'
|
||||
INACTIVE = 'inactive'
|
||||
PENDING = 'pending'
|
||||
|
||||
|
||||
class {ModelName}CreateSchema(BaseModel):
|
||||
"""
|
||||
Schema for creating a {ModelName}.
|
||||
|
||||
This schema defines the data contract for API requests.
|
||||
All validation rules are enforced here.
|
||||
"""
|
||||
|
||||
# Required string field with length constraints
|
||||
name: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=100,
|
||||
description="Display name",
|
||||
examples=["Example Name"]
|
||||
)
|
||||
|
||||
# Email field with built-in validation
|
||||
email: EmailStr = Field(
|
||||
...,
|
||||
description="Email address"
|
||||
)
|
||||
|
||||
# Optional string field
|
||||
description: Optional[str] = Field(
|
||||
None,
|
||||
max_length=500,
|
||||
description="Optional description"
|
||||
)
|
||||
|
||||
# Integer with range constraints
|
||||
quantity: int = Field(
|
||||
...,
|
||||
ge=1,
|
||||
le=999,
|
||||
description="Quantity (1-999)"
|
||||
)
|
||||
|
||||
# Decimal for currency (recommended over float)
|
||||
price: Decimal = Field(
|
||||
...,
|
||||
gt=0,
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
description="Price in USD"
|
||||
)
|
||||
|
||||
# Date field
|
||||
start_date: date = Field(
|
||||
...,
|
||||
description="Start date"
|
||||
)
|
||||
|
||||
# Enum field
|
||||
status: {ModelName}Status = Field(
|
||||
default={ModelName}Status.PENDING,
|
||||
description="Current status"
|
||||
)
|
||||
|
||||
# List field with constraints
|
||||
tags: List[str] = Field(
|
||||
default_factory=list,
|
||||
max_length=10,
|
||||
description="Associated tags"
|
||||
)
|
||||
|
||||
# Literal type (inline enum)
|
||||
priority: Literal['low', 'medium', 'high'] = Field(
|
||||
default='medium',
|
||||
description="Priority level"
|
||||
)
|
||||
|
||||
|
||||
# Field Validators
|
||||
# ---------------
|
||||
|
||||
@field_validator('name')
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
"""Validate name format."""
|
||||
if not v.strip():
|
||||
raise ValueError('Name cannot be empty')
|
||||
return v.strip()
|
||||
|
||||
@field_validator('tags')
|
||||
@classmethod
|
||||
def validate_unique_tags(cls, v: List[str]) -> List[str]:
|
||||
"""Ensure tags are unique."""
|
||||
if len(v) != len(set(v)):
|
||||
raise ValueError('Duplicate tags not allowed')
|
||||
return [tag.lower() for tag in v]
|
||||
|
||||
|
||||
# Model Validators (cross-field validation)
|
||||
# ----------------------------------------
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_business_rules(self):
|
||||
"""Validate business rules across fields."""
|
||||
# Example: Ensure high-priority items have descriptions
|
||||
if self.priority == 'high' and not self.description:
|
||||
raise ValueError('High priority items must have description')
|
||||
|
||||
return self
|
||||
|
||||
|
||||
# Configuration
|
||||
# ------------
|
||||
|
||||
model_config = {
|
||||
# Strip whitespace from strings
|
||||
'str_strip_whitespace': True,
|
||||
|
||||
# Validate on assignment
|
||||
'validate_assignment': True,
|
||||
|
||||
# JSON schema examples
|
||||
'json_schema_extra': {
|
||||
'examples': [{
|
||||
'name': 'Example {ModelName}',
|
||||
'email': 'example@company.com',
|
||||
'description': 'An example description',
|
||||
'quantity': 5,
|
||||
'price': '99.99',
|
||||
'start_date': '2024-01-01',
|
||||
'status': 'active',
|
||||
'tags': ['tag1', 'tag2'],
|
||||
'priority': 'medium'
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class {ModelName}UpdateSchema(BaseModel):
|
||||
"""
|
||||
Schema for updating a {ModelName}.
|
||||
|
||||
All fields are optional for partial updates.
|
||||
"""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
email: Optional[EmailStr] = None
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
quantity: Optional[int] = Field(None, ge=1, le=999)
|
||||
price: Optional[Decimal] = Field(None, gt=0, max_digits=10, decimal_places=2)
|
||||
status: Optional[{ModelName}Status] = None
|
||||
tags: Optional[List[str]] = Field(None, max_length=10)
|
||||
priority: Optional[Literal['low', 'medium', 'high']] = None
|
||||
|
||||
model_config = {
|
||||
'str_strip_whitespace': True,
|
||||
'validate_assignment': True
|
||||
}
|
||||
|
||||
|
||||
class {ModelName}ResponseSchema(BaseModel):
|
||||
"""
|
||||
Schema for {ModelName} responses.
|
||||
|
||||
Includes all fields plus auto-generated ones (id, timestamps).
|
||||
"""
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
email: str
|
||||
description: Optional[str]
|
||||
quantity: int
|
||||
price: Decimal
|
||||
start_date: date
|
||||
status: str
|
||||
tags: List[str]
|
||||
priority: str
|
||||
|
||||
# Auto-generated fields
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {
|
||||
# Enable ORM mode for SQLModel compatibility
|
||||
'from_attributes': True
|
||||
}
|
||||
|
||||
|
||||
# Usage Example
|
||||
# -------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Valid data
|
||||
data = {
|
||||
'name': 'Test Item',
|
||||
'email': 'test@example.com',
|
||||
'quantity': 10,
|
||||
'price': '49.99',
|
||||
'start_date': '2024-01-01',
|
||||
'status': 'active',
|
||||
'tags': ['electronics', 'featured']
|
||||
}
|
||||
|
||||
# Create instance
|
||||
item = {ModelName}CreateSchema(**data)
|
||||
print(f"Created: {item.model_dump_json()}")
|
||||
|
||||
# Validation will raise errors for invalid data
|
||||
try:
|
||||
invalid_data = {**data, 'quantity': 0} # Invalid quantity
|
||||
{ModelName}CreateSchema(**invalid_data)
|
||||
except ValidationError as e:
|
||||
print(f"Validation error: {e.errors()}")
|
||||
246
skills/data-validation/templates/sqlmodel-model.py
Normal file
246
skills/data-validation/templates/sqlmodel-model.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
SQLModel Database Model Template
|
||||
|
||||
Copy this template to create new database models.
|
||||
Replace {ModelName} and {table_name} with your actual values.
|
||||
"""
|
||||
|
||||
from sqlmodel import SQLModel, Field, Relationship
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from decimal import Decimal
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
|
||||
|
||||
# Optional: Define enum for status field
|
||||
class {ModelName}Status(str, Enum):
|
||||
"""Status enum for {ModelName}."""
|
||||
ACTIVE = 'active'
|
||||
INACTIVE = 'inactive'
|
||||
PENDING = 'pending'
|
||||
|
||||
|
||||
class {ModelName}(SQLModel, table=True):
|
||||
"""
|
||||
{ModelName} database model.
|
||||
|
||||
Represents {table_name} table in PostgreSQL.
|
||||
Includes multi-tenant isolation via tenant_id.
|
||||
"""
|
||||
|
||||
__tablename__ = '{table_name}'
|
||||
|
||||
# Primary Key
|
||||
# -----------
|
||||
id: UUID = Field(
|
||||
default_factory=uuid4,
|
||||
primary_key=True,
|
||||
description="Unique identifier"
|
||||
)
|
||||
|
||||
# Multi-Tenant Isolation (REQUIRED)
|
||||
# ---------------------------------
|
||||
tenant_id: UUID = Field(
|
||||
foreign_key="tenants.id",
|
||||
index=True,
|
||||
description="Tenant for RLS isolation"
|
||||
)
|
||||
|
||||
# Foreign Keys
|
||||
# -----------
|
||||
user_id: UUID = Field(
|
||||
foreign_key="users.id",
|
||||
index=True,
|
||||
description="Owner user ID"
|
||||
)
|
||||
|
||||
# Data Fields
|
||||
# ----------
|
||||
|
||||
# String fields with length constraints
|
||||
name: str = Field(
|
||||
max_length=100,
|
||||
index=True,
|
||||
description="Display name"
|
||||
)
|
||||
|
||||
email: str = Field(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
index=True,
|
||||
description="Email address"
|
||||
)
|
||||
|
||||
# Optional text field
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
max_length=500,
|
||||
description="Optional description"
|
||||
)
|
||||
|
||||
# Integer field
|
||||
quantity: int = Field(
|
||||
description="Quantity"
|
||||
)
|
||||
|
||||
# Decimal for currency
|
||||
price: Decimal = Field(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
description="Price in USD"
|
||||
)
|
||||
|
||||
# Enum/Status field
|
||||
status: str = Field(
|
||||
default='pending',
|
||||
max_length=20,
|
||||
index=True,
|
||||
description="Current status"
|
||||
)
|
||||
|
||||
# Boolean field
|
||||
is_active: bool = Field(
|
||||
default=True,
|
||||
description="Active flag"
|
||||
)
|
||||
|
||||
# JSON field (stored as JSONB in PostgreSQL)
|
||||
metadata: Optional[dict] = Field(
|
||||
default=None,
|
||||
sa_column_kwargs={"type_": "JSONB"},
|
||||
description="Additional metadata"
|
||||
)
|
||||
|
||||
# Timestamps (REQUIRED)
|
||||
# --------------------
|
||||
created_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
nullable=False,
|
||||
description="Creation timestamp"
|
||||
)
|
||||
|
||||
updated_at: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
nullable=False,
|
||||
description="Last update timestamp"
|
||||
)
|
||||
|
||||
# Relationships
|
||||
# ------------
|
||||
|
||||
# One-to-many: This model has many related items
|
||||
# items: List["RelatedItem"] = Relationship(back_populates="{model_name}")
|
||||
|
||||
# Many-to-one: This model belongs to a user
|
||||
# user: Optional["User"] = Relationship(back_populates="{model_name}s")
|
||||
|
||||
|
||||
# Related model example
|
||||
# ---------------------
|
||||
|
||||
class {ModelName}Item(SQLModel, table=True):
|
||||
"""Related items for {ModelName}."""
|
||||
|
||||
__tablename__ = '{table_name}_items'
|
||||
|
||||
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
||||
|
||||
# Foreign key to parent
|
||||
{model_name}_id: UUID = Field(
|
||||
foreign_key="{table_name}.id",
|
||||
index=True
|
||||
)
|
||||
|
||||
# Item fields
|
||||
name: str = Field(max_length=100)
|
||||
quantity: int = Field(gt=0)
|
||||
unit_price: Decimal = Field(max_digits=10, decimal_places=2)
|
||||
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Relationship back to parent
|
||||
{model_name}: Optional[{ModelName}] = Relationship(back_populates="items")
|
||||
|
||||
|
||||
# Update parent model with relationship
|
||||
{ModelName}.items = Relationship(back_populates="{model_name}")
|
||||
|
||||
|
||||
# Database Migration
|
||||
# ------------------
|
||||
|
||||
"""
|
||||
After creating this model, generate a migration:
|
||||
|
||||
```bash
|
||||
# Using Alembic
|
||||
alembic revision --autogenerate -m "create {table_name} table"
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
Migration will create:
|
||||
- Table {table_name}
|
||||
- Indexes on tenant_id, user_id, name, email, status
|
||||
- Unique constraint on email
|
||||
- Foreign key constraints
|
||||
- Timestamps with defaults
|
||||
"""
|
||||
|
||||
|
||||
# Row-Level Security (RLS)
|
||||
# ------------------------
|
||||
|
||||
"""
|
||||
Enable RLS for multi-tenant isolation:
|
||||
|
||||
```sql
|
||||
-- Enable RLS
|
||||
ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policy
|
||||
CREATE POLICY tenant_isolation ON {table_name}
|
||||
USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
||||
|
||||
-- Grant access
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON {table_name} TO app_user;
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
# Usage Example
|
||||
# -------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
from sqlmodel import Session, create_engine, select
|
||||
|
||||
# Create engine
|
||||
engine = create_engine('postgresql://user:pass@localhost/db')
|
||||
|
||||
# Create tables
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
# Insert data
|
||||
with Session(engine) as session:
|
||||
item = {ModelName}(
|
||||
tenant_id=uuid4(),
|
||||
user_id=uuid4(),
|
||||
name='Test Item',
|
||||
email='test@example.com',
|
||||
quantity=10,
|
||||
price=Decimal('49.99'),
|
||||
status='active'
|
||||
)
|
||||
|
||||
session.add(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
|
||||
print(f"Created {ModelName}: {item.id}")
|
||||
|
||||
# Query data
|
||||
with Session(engine) as session:
|
||||
statement = select({ModelName}).where({ModelName}.status == 'active')
|
||||
results = session.exec(statement).all()
|
||||
|
||||
print(f"Found {len(results)} active items")
|
||||
Reference in New Issue
Block a user