diff --git a/.planning/phases/08-stack-upgrade-backend-decomposition/08-02-SUMMARY.md b/.planning/phases/08-stack-upgrade-backend-decomposition/08-02-SUMMARY.md new file mode 100644 index 0000000..43f2172 --- /dev/null +++ b/.planning/phases/08-stack-upgrade-backend-decomposition/08-02-SUMMARY.md @@ -0,0 +1,103 @@ +--- +phase: 08-stack-upgrade-backend-decomposition +plan: "02" +subsystem: api +tags: [pydantic, schemas, refactor, cross-package, backend] + +# Dependency graph +requires: + - phase: none + provides: "Wave 0 prerequisite — no prior plan dependency" +provides: + - "backend/api/schemas.py: new cross-package Pydantic schemas module with CloudConnectionOut" + - "backend/api/cloud.py: no longer imports from api/admin (coupling eliminated)" +affects: + - "08-04-admin-split: plan 08-04 will delete original CloudConnectionOut from admin.py and import from api.schemas" + +# Tech tracking +tech-stack: + added: [] + patterns: + - "api/schemas.py as top-level home for Pydantic models shared across 2+ API packages (D-10)" + +key-files: + created: + - backend/api/schemas.py + modified: + - backend/api/cloud.py + +key-decisions: + - "CloudConnectionOut stays duplicated in admin.py until plan 08-04 admin split (intentional transient state)" + - "credentials_enc excluded from CloudConnectionOut field set — SEC-08 whitelist preserved verbatim" + - "coerce_id_to_str field_validator preserved byte-identically to prevent any UUID serialization regression" + +patterns-established: + - "backend/api/schemas.py: shared Pydantic models for 2+ packages live here, not in any single package" + +requirements-completed: [CODE-08] + +# Metrics +duration: 5min +completed: 2026-06-08 +--- + +# Phase 8 Plan 02: Shared Schemas Module Summary + +**New `backend/api/schemas.py` cross-package module with `CloudConnectionOut` extracted from `api/admin.py`; `api/cloud.py` import switched from `api.admin` to `api.schemas`, eliminating cross-package coupling (Pitfall 3)** + +## Performance + +- **Duration:** ~5 min +- **Started:** 2026-06-08 +- **Completed:** 2026-06-08 +- **Tasks:** 2 / 2 +- **Files modified:** 2 (1 created, 1 modified) + +## Accomplishments + +- Created `backend/api/schemas.py` as the canonical home for Pydantic response models shared across 2+ API packages (D-10) +- `CloudConnectionOut` copied verbatim from `api/admin.py` including SEC-08 docstring, 7-field whitelist, `from_attributes` config, and `coerce_id_to_str` field_validator +- Switched `backend/api/cloud.py` line 35 from `from api.admin import CloudConnectionOut` to `from api.schemas import CloudConnectionOut` +- All 51 cloud and admin tests continue to pass after the import switch +- Original `CloudConnectionOut` definition in `api/admin.py` left untouched — plan 08-04 owns the deletion during the admin split + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Create backend/api/schemas.py with CloudConnectionOut** - `10e0900` (feat) +2. **Task 2: Switch backend/api/cloud.py import to api.schemas** - `61fa6e2` (refactor) + +**Plan metadata:** (SUMMARY committed separately) + +## Files Created/Modified + +- `backend/api/schemas.py` — New cross-package Pydantic schemas module; contains `CloudConnectionOut` with SEC-08 whitelist, 7 fields, `coerce_id_to_str` validator +- `backend/api/cloud.py` — Single-line import change: `from api.admin` → `from api.schemas` + +## Decisions Made + +- Original `CloudConnectionOut` in `api/admin.py` is intentionally left in place until plan 08-04. Both definitions coexist temporarily; only `api/cloud.py` imports from `api/schemas`. This avoids a two-plan cascading dependency and is explicitly documented in T-08-02-03 as accepted transient duplication. +- No changes to any other file — plan scope held exactly. + +## Deviations from Plan + +None — plan executed exactly as written. + +## Issues Encountered + +- `tests/test_admin.py` does not exist; the correct file is `tests/test_admin_api.py`. Plan's verify command referenced the wrong filename, but the test run with the correct filename confirmed all 51 tests pass. No code change needed. + +## User Setup Required + +None — no external service configuration required. + +## Next Phase Readiness + +- `backend/api/schemas.py` is ready for plan 08-04 (admin split) to import from it and delete the original `CloudConnectionOut` from `api/admin.py` +- No blockers. Verification output: `51 passed, 5 warnings` from `pytest tests/test_cloud.py tests/test_admin_api.py -x -v` +- Final check: `grep -rn "from api.admin import CloudConnectionOut" backend/` returns no matches + +--- +*Phase: 08-stack-upgrade-backend-decomposition* +*Completed: 2026-06-08* diff --git a/backend/api/cloud.py b/backend/api/cloud.py index 27c8194..adcca7f 100644 --- a/backend/api/cloud.py +++ b/backend/api/cloud.py @@ -32,7 +32,7 @@ from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from api.admin import CloudConnectionOut +from api.schemas import CloudConnectionOut from config import settings from db.models import CloudConnection, User from deps.auth import get_regular_user diff --git a/backend/api/schemas.py b/backend/api/schemas.py new file mode 100644 index 0000000..f87b725 --- /dev/null +++ b/backend/api/schemas.py @@ -0,0 +1,42 @@ +"""Cross-package Pydantic response models. + +Models here are used by 2+ API packages and cannot live in a single package +without creating circular imports (D-10, RESEARCH.md Pitfall 3). +""" +from __future__ import annotations + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, field_validator + + +class CloudConnectionOut(BaseModel): + """SEC-08: credentials_enc deliberately excluded from this response model. + + Any admin or user endpoint returning CloudConnection ORM objects MUST use + this model to prevent accidental exposure of encrypted credentials. + Safe-by-default: whitelist of allowed fields (not blacklist). + + Moved from api/admin.py to eliminate cross-package coupling between + api/cloud.py and api/admin.py (D-10, RESEARCH.md Pitfall 3). + Used by api/cloud.py and api/admin/ (after plan 08-04 admin split). + + Note: id is declared as str and coerced via validator so UUID ORM values + serialize correctly without json_encoders. + """ + + id: str + provider: str + display_name: str + status: str + connected_at: datetime + server_url: Optional[str] = None + connection_username: Optional[str] = None + model_config = {"from_attributes": True} + + @field_validator("id", mode="before") + @classmethod + def coerce_id_to_str(cls, v) -> str: + """Coerce UUID objects to str so the model validates from ORM instances.""" + return str(v)