feat(arch): add API-007 rule to enforce layered architecture
Add architecture rule that detects when API routes import database models directly, enforcing Routes → Services → Models pattern. Changes: - Add API-007 rule to .architecture-rules/api.yaml - Add _check_no_model_imports() validation to validator script - Update customer imports to use canonical module location - Add storefront module restructure implementation plan The validator now detects 81 violations across 67 API files where database models are imported directly instead of going through services. This is Phase 1 of the storefront restructure plan. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -204,3 +204,53 @@ api_endpoint_rules:
|
|||||||
file_pattern: "app/api/v1/shop/**/*.py"
|
file_pattern: "app/api/v1/shop/**/*.py"
|
||||||
discouraged_patterns:
|
discouraged_patterns:
|
||||||
- "db.query(.*).all()"
|
- "db.query(.*).all()"
|
||||||
|
|
||||||
|
- id: "API-007"
|
||||||
|
name: "API endpoints must NOT import database models directly"
|
||||||
|
severity: "error"
|
||||||
|
description: |
|
||||||
|
API endpoints must follow the layered architecture: Routes → Services → Models.
|
||||||
|
Routes should NEVER import database models directly.
|
||||||
|
|
||||||
|
WHY THIS MATTERS:
|
||||||
|
- Layered architecture: Each layer has clear responsibilities
|
||||||
|
- Testability: Routes can be tested without database
|
||||||
|
- Flexibility: Database schema changes don't affect route signatures
|
||||||
|
- Type safety: Schemas define the API contract, not database structure
|
||||||
|
|
||||||
|
For dependency injection (e.g., current user/customer), use schemas instead
|
||||||
|
of database models as return types.
|
||||||
|
|
||||||
|
WRONG:
|
||||||
|
from models.database.customer import Customer
|
||||||
|
from models.database.order import Order
|
||||||
|
from app.modules.customers.models.customer import Customer
|
||||||
|
|
||||||
|
@router.get("/orders")
|
||||||
|
def get_orders(customer: Customer = Depends(get_current_customer)):
|
||||||
|
...
|
||||||
|
|
||||||
|
RIGHT:
|
||||||
|
from app.modules.customers.schemas import CustomerContext
|
||||||
|
from app.modules.orders.services import order_service
|
||||||
|
|
||||||
|
@router.get("/orders")
|
||||||
|
def get_orders(customer: CustomerContext = Depends(get_current_customer)):
|
||||||
|
return order_service.get_orders(db, customer.id, customer.vendor_id)
|
||||||
|
|
||||||
|
ALLOWED IMPORTS IN API ROUTES:
|
||||||
|
- Schemas: from models.schema.* or from app.modules.*.schemas
|
||||||
|
- Services: from app.services.* or from app.modules.*.services
|
||||||
|
- Dependencies: from app.api.deps
|
||||||
|
- Database session: from app.core.database import get_db
|
||||||
|
|
||||||
|
NOT ALLOWED:
|
||||||
|
- from models.database.*
|
||||||
|
- from app.modules.*.models.*
|
||||||
|
pattern:
|
||||||
|
file_pattern: "app/api/**/*.py"
|
||||||
|
anti_patterns:
|
||||||
|
- "from models\\.database\\."
|
||||||
|
- "from app\\.modules\\.[a-z_]+\\.models\\."
|
||||||
|
exclude_files:
|
||||||
|
- "app/api/deps.py" # Dependencies may need model access for queries
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ except ImportError as e:
|
|||||||
# CUSTOMER MODELS
|
# CUSTOMER MODELS
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
try:
|
try:
|
||||||
from models.database.customer import Customer, CustomerAddress
|
from app.modules.customers.models.customer import Customer, CustomerAddress
|
||||||
|
|
||||||
print(" ✓ Customer models imported (2 models)")
|
print(" ✓ Customer models imported (2 models)")
|
||||||
print(" - Customer")
|
print(" - Customer")
|
||||||
|
|||||||
@@ -740,7 +740,7 @@ def _validate_customer_token(token: str, request: Request, db: Session):
|
|||||||
|
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
|
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
|
|
||||||
# Decode and validate customer JWT token
|
# Decode and validate customer JWT token
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from sqlalchemy import func
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.exceptions import AdminOperationException, VendorNotFoundException
|
from app.exceptions import AdminOperationException, VendorNotFoundException
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
from models.database.inventory import Inventory
|
from models.database.inventory import Inventory
|
||||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||||
from models.database.marketplace_product import MarketplaceProduct
|
from models.database.marketplace_product import MarketplaceProduct
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class CardService:
|
|||||||
Returns:
|
Returns:
|
||||||
(cards, total_count)
|
(cards, total_count)
|
||||||
"""
|
"""
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
db.query(LoyaltyCard)
|
db.query(LoyaltyCard)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from app.modules.messaging.models.message import (
|
|||||||
MessageAttachment,
|
MessageAttachment,
|
||||||
ParticipantType,
|
ParticipantType,
|
||||||
)
|
)
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -1122,7 +1122,7 @@ class EmailService:
|
|||||||
|
|
||||||
# 2. Customer's preferred language
|
# 2. Customer's preferred language
|
||||||
if customer_id:
|
if customer_id:
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
|
|
||||||
customer = (
|
customer = (
|
||||||
self.db.query(Customer).filter(Customer.id == customer_id).first()
|
self.db.query(Customer).filter(Customer.id == customer_id).first()
|
||||||
|
|||||||
344
docs/proposals/PLAN_storefront-module-restructure.md
Normal file
344
docs/proposals/PLAN_storefront-module-restructure.md
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
# Implementation Plan: Storefront Module Restructure
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Restructure the platform to properly separate **core platform concerns** (customer auth/identity) from **e-commerce concerns** (cart, checkout, catalog), and rename "shop" to "storefront" throughout.
|
||||||
|
|
||||||
|
## Current Issues
|
||||||
|
|
||||||
|
1. **API routes import database models directly** - violates layered architecture
|
||||||
|
2. **No architecture rule enforces this** - validator doesn't catch it
|
||||||
|
3. **"shop" naming is misleading** - not all platforms sell items
|
||||||
|
4. **Customer auth is bundled with e-commerce** - should be core/platform concern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Add Missing Architecture Rule
|
||||||
|
|
||||||
|
### Task 1.1: Add API-007 rule to prevent direct model imports
|
||||||
|
|
||||||
|
**File:** `.architecture-rules/api.yaml`
|
||||||
|
|
||||||
|
Add new rule:
|
||||||
|
```yaml
|
||||||
|
- id: "API-007"
|
||||||
|
name: "API endpoints must NOT import database models directly"
|
||||||
|
severity: "error"
|
||||||
|
description: |
|
||||||
|
API endpoints should import from services, not directly from database models.
|
||||||
|
This enforces the layered architecture: Routes → Services → Models.
|
||||||
|
|
||||||
|
The only exception is for type hints in FastAPI dependency injection,
|
||||||
|
which should use schemas or services that return the appropriate types.
|
||||||
|
|
||||||
|
WRONG:
|
||||||
|
from models.database.customer import Customer
|
||||||
|
from models.database.order import Order
|
||||||
|
|
||||||
|
RIGHT:
|
||||||
|
from app.modules.customers.services import customer_service
|
||||||
|
from models.schema.customer import CustomerResponse
|
||||||
|
pattern:
|
||||||
|
file_pattern: "app/api/**/*.py"
|
||||||
|
anti_patterns:
|
||||||
|
- "from models\\.database\\."
|
||||||
|
- "from app\\.modules\\..*\\.models\\."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 1.2: Update validator to check this rule
|
||||||
|
|
||||||
|
**File:** `scripts/validate_architecture.py`
|
||||||
|
|
||||||
|
Add validation for API-007.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Rename "shop" to "storefront"
|
||||||
|
|
||||||
|
### Task 2.1: Rename API directory
|
||||||
|
```
|
||||||
|
app/api/v1/shop/ → app/api/v1/storefront/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2.2: Update all imports referencing shop
|
||||||
|
- Update router registrations in `app/api/v1/__init__.py`
|
||||||
|
- Update any references in middleware
|
||||||
|
- Update documentation
|
||||||
|
|
||||||
|
### Task 2.3: Rename routes/pages if applicable
|
||||||
|
```
|
||||||
|
app/routes/shop_pages.py → app/routes/storefront_pages.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Create New E-commerce Modules
|
||||||
|
|
||||||
|
### Task 3.1: Create `cart` module
|
||||||
|
```
|
||||||
|
app/modules/cart/
|
||||||
|
├── __init__.py
|
||||||
|
├── definition.py
|
||||||
|
├── config.py
|
||||||
|
├── exceptions.py
|
||||||
|
├── models/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── cart.py # Cart, CartItem models (if persistent)
|
||||||
|
├── schemas/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── cart.py # Move from models/schema/cart.py
|
||||||
|
├── services/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── cart_service.py # Move from app/services/cart_service.py
|
||||||
|
└── routes/
|
||||||
|
└── api/
|
||||||
|
├── __init__.py
|
||||||
|
└── storefront.py # Cart endpoints for customers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3.2: Create `checkout` module
|
||||||
|
```
|
||||||
|
app/modules/checkout/
|
||||||
|
├── __init__.py
|
||||||
|
├── definition.py
|
||||||
|
├── config.py
|
||||||
|
├── exceptions.py
|
||||||
|
├── schemas/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── checkout.py # CheckoutRequest, CheckoutResponse
|
||||||
|
├── services/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── checkout_service.py # Orchestrates cart → order conversion
|
||||||
|
└── routes/
|
||||||
|
└── api/
|
||||||
|
├── __init__.py
|
||||||
|
└── storefront.py # Place order endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3.3: Create `catalog` module
|
||||||
|
```
|
||||||
|
app/modules/catalog/
|
||||||
|
├── __init__.py
|
||||||
|
├── definition.py
|
||||||
|
├── config.py
|
||||||
|
├── schemas/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── catalog.py # ProductListResponse, ProductDetailResponse
|
||||||
|
├── services/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── catalog_service.py # Customer-facing product queries
|
||||||
|
└── routes/
|
||||||
|
└── api/
|
||||||
|
├── __init__.py
|
||||||
|
└── storefront.py # Product browsing endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Move Storefront Routes to Modules
|
||||||
|
|
||||||
|
### Task 4.1: Move customer auth routes to `customers` module
|
||||||
|
|
||||||
|
**From:** `app/api/v1/storefront/auth.py`
|
||||||
|
**To:** `app/modules/customers/routes/api/storefront.py`
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
- `POST /auth/register` - Customer registration
|
||||||
|
- `POST /auth/login` - Customer login
|
||||||
|
- `POST /auth/logout` - Customer logout
|
||||||
|
- `POST /auth/password-reset/request` - Request password reset
|
||||||
|
- `POST /auth/password-reset/confirm` - Confirm password reset
|
||||||
|
|
||||||
|
### Task 4.2: Move customer profile routes to `customers` module
|
||||||
|
|
||||||
|
**From:** `app/api/v1/storefront/profile.py`
|
||||||
|
**To:** `app/modules/customers/routes/api/storefront.py` (append)
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
- `GET /profile` - Get customer profile
|
||||||
|
- `PUT /profile` - Update customer profile
|
||||||
|
|
||||||
|
### Task 4.3: Move customer address routes to `customers` module
|
||||||
|
|
||||||
|
**From:** `app/api/v1/storefront/addresses.py`
|
||||||
|
**To:** `app/modules/customers/routes/api/storefront.py` (append)
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
- `GET /addresses` - List customer addresses
|
||||||
|
- `POST /addresses` - Create address
|
||||||
|
- `PUT /addresses/{id}` - Update address
|
||||||
|
- `DELETE /addresses/{id}` - Delete address
|
||||||
|
|
||||||
|
### Task 4.4: Move cart routes to `cart` module
|
||||||
|
|
||||||
|
**From:** `app/api/v1/storefront/carts.py`
|
||||||
|
**To:** `app/modules/cart/routes/api/storefront.py`
|
||||||
|
|
||||||
|
### Task 4.5: Move order placement to `checkout` module
|
||||||
|
|
||||||
|
**From:** `app/api/v1/storefront/orders.py` (POST /orders only)
|
||||||
|
**To:** `app/modules/checkout/routes/api/storefront.py`
|
||||||
|
|
||||||
|
### Task 4.6: Move order viewing to `orders` module
|
||||||
|
|
||||||
|
**From:** `app/api/v1/storefront/orders.py` (GET endpoints)
|
||||||
|
**To:** `app/modules/orders/routes/api/storefront.py`
|
||||||
|
|
||||||
|
### Task 4.7: Move product browsing to `catalog` module
|
||||||
|
|
||||||
|
**From:** `app/api/v1/storefront/products.py`
|
||||||
|
**To:** `app/modules/catalog/routes/api/storefront.py`
|
||||||
|
|
||||||
|
### Task 4.8: Move messaging routes to `messaging` module
|
||||||
|
|
||||||
|
**From:** `app/api/v1/storefront/messages.py`
|
||||||
|
**To:** `app/modules/messaging/routes/api/storefront.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Fix Direct Model Imports
|
||||||
|
|
||||||
|
### Task 5.1: Update dependency injection pattern
|
||||||
|
|
||||||
|
**Current pattern (violates layered architecture):**
|
||||||
|
```python
|
||||||
|
from models.database.customer import Customer
|
||||||
|
|
||||||
|
@router.get("/orders")
|
||||||
|
def get_orders(customer: Customer = Depends(get_current_customer_api)):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**New pattern (proper layered architecture):**
|
||||||
|
```python
|
||||||
|
from app.modules.customers.schemas import CustomerContext
|
||||||
|
|
||||||
|
@router.get("/orders")
|
||||||
|
def get_orders(customer: CustomerContext = Depends(get_current_customer_api)):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5.2: Create `CustomerContext` schema
|
||||||
|
|
||||||
|
**File:** `app/modules/customers/schemas/context.py`
|
||||||
|
```python
|
||||||
|
class CustomerContext(BaseModel):
|
||||||
|
"""Customer context for dependency injection in storefront routes."""
|
||||||
|
id: int
|
||||||
|
vendor_id: int
|
||||||
|
email: str
|
||||||
|
first_name: str | None
|
||||||
|
last_name: str | None
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5.3: Update `get_current_customer_api` dependency
|
||||||
|
|
||||||
|
**File:** `app/api/deps.py`
|
||||||
|
|
||||||
|
Change return type from `Customer` (database model) to `CustomerContext` (schema).
|
||||||
|
|
||||||
|
### Task 5.4: Update all storefront routes
|
||||||
|
|
||||||
|
Replace all `Customer` type hints with `CustomerContext`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Delete Legacy Files
|
||||||
|
|
||||||
|
### Task 6.1: Delete `models/database/customer.py`
|
||||||
|
|
||||||
|
After all imports are updated to use `app.modules.customers.models.customer`.
|
||||||
|
|
||||||
|
### Task 6.2: Delete `app/api/v1/storefront/` directory
|
||||||
|
|
||||||
|
After all routes are moved to their respective modules.
|
||||||
|
|
||||||
|
### Task 6.3: Delete `app/services/cart_service.py`
|
||||||
|
|
||||||
|
After migrated to `app/modules/cart/services/cart_service.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: Update Documentation
|
||||||
|
|
||||||
|
### Task 7.1: Update API documentation
|
||||||
|
- Rename all "shop" references to "storefront"
|
||||||
|
- Update endpoint paths
|
||||||
|
|
||||||
|
### Task 7.2: Update architecture documentation
|
||||||
|
- Document the layered architecture rule
|
||||||
|
- Document module structure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module Dependency Graph (Final State)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ core │
|
||||||
|
│ (tenancy) │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
┌────────────┼────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||||
|
│customers │ │ payments │ │messaging │
|
||||||
|
│ (auth) │ │ │ │ │
|
||||||
|
└────┬─────┘ └────┬─────┘ └──────────┘
|
||||||
|
│ │
|
||||||
|
│ ┌────────┴────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────┐ ┌──────────┐
|
||||||
|
│ cart │ │ orders │
|
||||||
|
│ │ │ │
|
||||||
|
└────┬─────┘ └────┬─────┘
|
||||||
|
│ │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────┐
|
||||||
|
│ checkout │
|
||||||
|
│ │
|
||||||
|
└──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
|
||||||
|
1. **Phase 1** - Add architecture rule (enables detection)
|
||||||
|
2. **Phase 2** - Rename shop → storefront (terminology)
|
||||||
|
3. **Phase 3** - Create new modules (cart, checkout, catalog)
|
||||||
|
4. **Phase 4** - Move routes to modules
|
||||||
|
5. **Phase 5** - Fix direct model imports
|
||||||
|
6. **Phase 6** - Delete legacy files
|
||||||
|
7. **Phase 7** - Update documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified Summary
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `.architecture-rules/api.yaml` (modified - add API-007)
|
||||||
|
- `app/modules/cart/**` (new module)
|
||||||
|
- `app/modules/checkout/**` (new module)
|
||||||
|
- `app/modules/catalog/**` (new module)
|
||||||
|
- `app/modules/customers/routes/api/storefront.py`
|
||||||
|
- `app/modules/customers/schemas/context.py`
|
||||||
|
- `app/modules/orders/routes/api/storefront.py`
|
||||||
|
- `app/modules/messaging/routes/api/storefront.py`
|
||||||
|
|
||||||
|
### Deleted Files
|
||||||
|
- `app/api/v1/shop/` (entire directory)
|
||||||
|
- `app/routes/shop_pages.py`
|
||||||
|
- `models/database/customer.py`
|
||||||
|
- `app/services/cart_service.py`
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `app/api/deps.py` (CustomerContext return type)
|
||||||
|
- `scripts/validate_architecture.py` (add API-007 check)
|
||||||
|
- All files currently importing from legacy locations
|
||||||
@@ -54,7 +54,7 @@ from middleware.auth import AuthManager
|
|||||||
from app.modules.cms.models import ContentPage
|
from app.modules.cms.models import ContentPage
|
||||||
from models.database.admin import PlatformAlert
|
from models.database.admin import PlatformAlert
|
||||||
from models.database.company import Company
|
from models.database.company import Company
|
||||||
from models.database.customer import Customer, CustomerAddress
|
from app.modules.customers.models.customer import Customer, CustomerAddress
|
||||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||||
from models.database.marketplace_product import MarketplaceProduct
|
from models.database.marketplace_product import MarketplaceProduct
|
||||||
from models.database.marketplace_product_translation import (
|
from models.database.marketplace_product_translation import (
|
||||||
|
|||||||
@@ -1771,6 +1771,9 @@ class ArchitectureValidator:
|
|||||||
# API-005: Check vendor_id scoping for vendor/shop endpoints
|
# API-005: Check vendor_id scoping for vendor/shop endpoints
|
||||||
self._check_vendor_scoping(file_path, content, lines)
|
self._check_vendor_scoping(file_path, content, lines)
|
||||||
|
|
||||||
|
# API-007: Check for direct model imports
|
||||||
|
self._check_no_model_imports(file_path, content, lines)
|
||||||
|
|
||||||
def _check_pydantic_usage(self, file_path: Path, content: str, lines: list[str]):
|
def _check_pydantic_usage(self, file_path: Path, content: str, lines: list[str]):
|
||||||
"""API-001: Ensure endpoints use Pydantic models"""
|
"""API-001: Ensure endpoints use Pydantic models"""
|
||||||
rule = self._get_rule("API-001")
|
rule = self._get_rule("API-001")
|
||||||
@@ -2061,6 +2064,59 @@ class ArchitectureValidator:
|
|||||||
)
|
)
|
||||||
return # Only report once per file
|
return # Only report once per file
|
||||||
|
|
||||||
|
def _check_no_model_imports(
|
||||||
|
self, file_path: Path, content: str, lines: list[str]
|
||||||
|
):
|
||||||
|
"""API-007: Check that API endpoints do NOT import database models directly.
|
||||||
|
|
||||||
|
Routes should follow layered architecture: Routes → Services → Models.
|
||||||
|
Database models should only be imported in services, not in API routes.
|
||||||
|
|
||||||
|
Allowed: schemas, services, deps, database session
|
||||||
|
Not allowed: models.database.*, app.modules.*.models.*
|
||||||
|
"""
|
||||||
|
rule = self._get_rule("API-007")
|
||||||
|
if not rule:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Skip deps.py - it may need model access for queries
|
||||||
|
file_path_str = str(file_path)
|
||||||
|
if file_path_str.endswith("deps.py"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for noqa
|
||||||
|
if "noqa: api-007" in content.lower():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Patterns that indicate direct model imports
|
||||||
|
model_import_patterns = [
|
||||||
|
(r"from models\.database\.", "Importing from legacy models.database location"),
|
||||||
|
(r"from app\.modules\.[a-z_]+\.models\.", "Importing from module models"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, line in enumerate(lines, 1):
|
||||||
|
# Skip comments
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for noqa on this specific line
|
||||||
|
if "noqa: api-007" in line.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for pattern, message in model_import_patterns:
|
||||||
|
if re.search(pattern, line):
|
||||||
|
self._add_violation(
|
||||||
|
rule_id="API-007",
|
||||||
|
rule_name="API endpoints must NOT import database models directly",
|
||||||
|
severity=Severity.ERROR,
|
||||||
|
file_path=file_path,
|
||||||
|
line_number=i,
|
||||||
|
message=message,
|
||||||
|
context=line.strip()[:80],
|
||||||
|
suggestion="Import from services or schemas instead. Use CustomerContext schema for dependency injection.",
|
||||||
|
)
|
||||||
|
|
||||||
def _validate_service_layer(self, target_path: Path):
|
def _validate_service_layer(self, target_path: Path):
|
||||||
"""Validate service layer rules (SVC-001 to SVC-007)"""
|
"""Validate service layer rules (SVC-001 to SVC-007)"""
|
||||||
print("🔧 Validating service layer...")
|
print("🔧 Validating service layer...")
|
||||||
|
|||||||
2
tests/fixtures/customer_fixtures.py
vendored
2
tests/fixtures/customer_fixtures.py
vendored
@@ -8,7 +8,7 @@ See tests/conftest.py for details on fixture best practices.
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from models.database.customer import Customer, CustomerAddress
|
from app.modules.customers.models.customer import Customer, CustomerAddress
|
||||||
from models.database.order import Order
|
from models.database.order import Order
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
|
|
||||||
from models.database.customer import Customer, CustomerAddress
|
from app.modules.customers.models.customer import Customer, CustomerAddress
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from unittest.mock import patch, MagicMock
|
|||||||
import pytest
|
import pytest
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
|
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
from models.database.invoice import Invoice, InvoiceStatus, VendorInvoiceSettings
|
from models.database.invoice import Invoice, InvoiceStatus, VendorInvoiceSettings
|
||||||
from models.database.order import Order, OrderItem
|
from models.database.order import Order, OrderItem
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
from models.database.password_reset_token import PasswordResetToken
|
from models.database.password_reset_token import PasswordResetToken
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
from models.database.invoice import Invoice, InvoiceStatus, VendorInvoiceSettings
|
from models.database.invoice import Invoice, InvoiceStatus, VendorInvoiceSettings
|
||||||
from models.database.order import Order
|
from models.database.order import Order
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from models.database.customer import Customer, CustomerAddress
|
from app.modules.customers.models.customer import Customer, CustomerAddress
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import pytest
|
|||||||
|
|
||||||
from app.exceptions.customer import CustomerNotFoundException
|
from app.exceptions.customer import CustomerNotFoundException
|
||||||
from app.services.admin_customer_service import AdminCustomerService
|
from app.services.admin_customer_service import AdminCustomerService
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import pytest
|
|||||||
|
|
||||||
from app.exceptions import AddressLimitExceededException, AddressNotFoundException
|
from app.exceptions import AddressLimitExceededException, AddressNotFoundException
|
||||||
from app.services.customer_address_service import CustomerAddressService
|
from app.services.customer_address_service import CustomerAddressService
|
||||||
from models.database.customer import CustomerAddress
|
from app.modules.customers.models.customer import CustomerAddress
|
||||||
from models.schema.customer import CustomerAddressCreate, CustomerAddressUpdate
|
from models.schema.customer import CustomerAddressCreate, CustomerAddressUpdate
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from app.services.order_service import (
|
|||||||
OrderService,
|
OrderService,
|
||||||
order_service,
|
order_service,
|
||||||
)
|
)
|
||||||
from models.database.customer import Customer
|
from app.modules.customers.models.customer import Customer
|
||||||
from models.database.order import Order, OrderItem
|
from models.database.order import Order, OrderItem
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user