462 lines
14 KiB
Markdown
462 lines
14 KiB
Markdown
# Implementation Tasks: Add GraphQL Member API
|
|||
|
|
|
||
|
|
**Change ID:** `add-graphql-member-api`
|
||
|
|
**Status:** Draft
|
||
|
|
**Created:** 2025-11-20
|
||
|
|
|
||
|
|
## Task Breakdown
|
||
|
|
|
||
|
|
This change is implemented through ~18 discrete, verifiable tasks. Each task delivers user-visible progress and includes validation steps.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 1: Project Foundation
|
||
|
|
|
||
|
|
### Task 1: Initialize Python project with uv
|
||
|
|
|
||
|
|
**Objective:** Create pyproject.toml and configure uv for dependency management
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Run `uv init` to create basic project structure
|
||
|
|
2. Edit pyproject.toml to set name="clubber", version="0.1.0"
|
||
|
|
3. Set Python requirement to ">=3.11"
|
||
|
|
4. Add project metadata (description, authors, license)
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] `uv sync` completes successfully
|
||
|
|
- [ ] pyproject.toml contains project metadata
|
||
|
|
|
||
|
|
**Dependencies:** None
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 2: Add core dependencies
|
||
|
|
|
||
|
|
**Objective:** Install FastAPI, Strawberry, SQLAlchemy, and Alembic
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Run `uv add fastapi uvicorn[standard]`
|
||
|
|
2. Run `uv add strawberry-graphql[fastapi]`
|
||
|
|
3. Run `uv add sqlalchemy[asyncio] aiosqlite`
|
||
|
|
4. Run `uv add alembic pydantic-settings`
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] All packages appear in pyproject.toml dependencies
|
||
|
|
- [ ] `uv sync` resolves dependencies without conflicts
|
||
|
|
- [ ] uv.lock file is generated
|
||
|
|
|
||
|
|
**Dependencies:** Task 1
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 3: Add development dependencies
|
||
|
|
|
||
|
|
**Objective:** Install testing and code quality tools
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Run `uv add --dev pytest pytest-asyncio`
|
||
|
|
2. Run `uv add --dev black ruff isort`
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] Development packages in [tool.uv.dev-dependencies] or similar
|
||
|
|
- [ ] `uv run pytest --version` works
|
||
|
|
- [ ] `uv run black --version` works
|
||
|
|
|
||
|
|
**Dependencies:** Task 1
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 4: Configure development tools in pyproject.toml
|
||
|
|
|
||
|
|
**Objective:** Set up black, ruff, and isort configurations
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Add [tool.black] section with line-length=88, target-version=["py311"]
|
||
|
|
2. Add [tool.ruff] section with line-length=88, select=["E", "F", "W", "I"]
|
||
|
|
3. Add [tool.isort] section with profile="black"
|
||
|
|
4. Add [tool.pytest.ini_options] with asyncio_mode="auto"
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] `uv run black --check .` runs (passes even if no code yet)
|
||
|
|
- [ ] `uv run ruff check .` runs
|
||
|
|
- [ ] Tool configurations are in pyproject.toml
|
||
|
|
|
||
|
|
**Dependencies:** Task 3
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 5: Create src directory structure
|
||
|
|
|
||
|
|
**Objective:** Set up layered architecture directories
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Create directories: `src/models/`, `src/schemas/`, `src/resolvers/`
|
||
|
|
2. Create empty `__init__.py` in each directory
|
||
|
|
3. Create `src/main.py`, `src/config.py`, `src/database.py` as placeholders
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] Directory structure matches design.md
|
||
|
|
- [ ] All directories have __init__.py files
|
||
|
|
- [ ] `ls -R src/` shows complete structure
|
||
|
|
|
||
|
|
**Dependencies:** Task 1
|
||
|
|
|
||
|
|
**Can run in parallel with:** Task 2, Task 3
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 2: Database Layer
|
||
|
|
|
||
|
|
### Task 6: Create configuration management
|
||
|
|
|
||
|
|
**Objective:** Implement Settings class with environment variable support
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. In `src/config.py`, create Settings class using pydantic-settings
|
||
|
|
2. Add fields: database_url (default: "sqlite+aiosqlite:///./clubber.db"), debug (default: False)
|
||
|
|
3. Configure .env file loading
|
||
|
|
4. Create `.env.example` with documented settings
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] Settings() instantiates with defaults
|
||
|
|
- [ ] DATABASE_URL environment variable overrides default
|
||
|
|
- [ ] .env.example exists and documents all settings
|
||
|
|
|
||
|
|
**Dependencies:** Task 2, Task 5
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 7: Set up database connection and session factory
|
||
|
|
|
||
|
|
**Objective:** Create async SQLAlchemy engine and session management
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. In `src/database.py`, import SQLAlchemy async components
|
||
|
|
2. Create async_engine using settings.database_url
|
||
|
|
3. Create async_session_maker with AsyncSession
|
||
|
|
4. Implement get_db_session() dependency for FastAPI
|
||
|
|
5. Create declarative Base for models
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] async_engine is created without errors
|
||
|
|
- [ ] async_session_maker is callable
|
||
|
|
- [ ] get_db_session yields AsyncSession
|
||
|
|
|
||
|
|
**Dependencies:** Task 6
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 8: Define Member SQLAlchemy model
|
||
|
|
|
||
|
|
**Objective:** Create database model for members table with minimal required fields
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. In `src/models/member.py`, import SQLAlchemy components
|
||
|
|
2. Define Member class inheriting from Base
|
||
|
|
3. Add id field (primary key, auto-increment)
|
||
|
|
4. Add first_name field (nullable=False, required)
|
||
|
|
5. Add optional fields with nullable=True: last_name, street, apartment_number, zip, city, country, email, phone
|
||
|
|
6. Add timestamps: created_at, updated_at with defaults
|
||
|
|
7. Set __tablename__ = "members"
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] Member class has all fields defined
|
||
|
|
- [ ] Only first_name has nullable=False
|
||
|
|
- [ ] All other data fields have nullable=True
|
||
|
|
- [ ] Field types and length constraints match spec
|
||
|
|
- [ ] `from src.models.member import Member` works
|
||
|
|
|
||
|
|
**Dependencies:** Task 7
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 9: Initialize Alembic for migrations
|
||
|
|
|
||
|
|
**Objective:** Set up Alembic configuration for database versioning
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Run `uv run alembic init migrations`
|
||
|
|
2. Edit `migrations/env.py` to import Base and use async operations
|
||
|
|
3. Edit `alembic.ini` to use config.py for database URL
|
||
|
|
4. Update env.py to reference all models (import src.models.member)
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] `migrations/` directory exists
|
||
|
|
- [ ] `alembic.ini` is configured
|
||
|
|
- [ ] `uv run alembic current` executes without errors
|
||
|
|
|
||
|
|
**Dependencies:** Task 8
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 10: Create initial database migration
|
||
|
|
|
||
|
|
**Objective:** Generate migration to create members table
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Run `uv run alembic revision --autogenerate -m "create members table"`
|
||
|
|
2. Review generated migration in `migrations/versions/`
|
||
|
|
3. Verify upgrade() creates members table with all columns
|
||
|
|
4. Verify downgrade() drops members table
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] Migration file exists in migrations/versions/
|
||
|
|
- [ ] Migration includes all Member model fields
|
||
|
|
- [ ] `uv run alembic upgrade head` creates database
|
||
|
|
- [ ] clubber.db file exists with members table
|
||
|
|
|
||
|
|
**Dependencies:** Task 9
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 3: GraphQL API Layer
|
||
|
|
|
||
|
|
### Task 11: Define Strawberry GraphQL types
|
||
|
|
|
||
|
|
**Objective:** Create GraphQL schema types with minimal required fields
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. In `src/schemas/member.py`, import strawberry
|
||
|
|
2. Define @strawberry.type Member with camelCase fields (only firstName, createdAt, updatedAt as non-null)
|
||
|
|
3. Define @strawberry.input CreateMemberInput (only firstName required)
|
||
|
|
4. Define @strawberry.input UpdateMemberInput (all fields optional except id)
|
||
|
|
5. Map snake_case Python to camelCase GraphQL with field aliases
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] Member type has all fields from spec with correct nullability
|
||
|
|
- [ ] CreateMemberInput requires only firstName
|
||
|
|
- [ ] All other fields are optional (nullable GraphQL types)
|
||
|
|
- [ ] `from src.schemas.member import Member` works
|
||
|
|
|
||
|
|
**Dependencies:** Task 2
|
||
|
|
|
||
|
|
**Can run in parallel with:** Database layer tasks (Task 6-10)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 12: Implement validation helpers
|
||
|
|
|
||
|
|
**Objective:** Create conditional validation functions for optional fields
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Create `src/validation.py` (or add to schemas)
|
||
|
|
2. Define EMAIL_PATTERN and PHONE_PATTERN regex
|
||
|
|
3. Implement validate_email(email: Optional[str]) with conditional logic (only validate if provided)
|
||
|
|
4. Implement validate_phone(phone: Optional[str]) with conditional logic (only validate if provided)
|
||
|
|
5. Implement validate_first_name(first_name: str) to ensure non-empty
|
||
|
|
6. Create ValidationError custom exception
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] validate_email("test@example.com") passes
|
||
|
|
- [ ] validate_email(None) passes (no validation)
|
||
|
|
- [ ] validate_email("invalid") raises ValidationError
|
||
|
|
- [ ] validate_phone("+15551234567") passes
|
||
|
|
- [ ] validate_phone(None) passes (no validation)
|
||
|
|
- [ ] validate_phone("123") raises ValidationError
|
||
|
|
- [ ] validate_first_name("") raises ValidationError
|
||
|
|
|
||
|
|
**Dependencies:** Task 11
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 13: Implement Query resolvers
|
||
|
|
|
||
|
|
**Objective:** Create GraphQL query resolvers for reading members
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. In `src/resolvers/member.py`, import necessary components
|
||
|
|
2. Define Query class with @strawberry.type
|
||
|
|
3. Implement member(id: ID) -> Optional[Member] resolver
|
||
|
|
4. Implement members() -> List[Member] resolver with sorting
|
||
|
|
5. Use get_db_session dependency for database access
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] Resolvers are async functions
|
||
|
|
- [ ] Database queries use SQLAlchemy 2.0 select() syntax
|
||
|
|
- [ ] Resolvers convert DB models to GraphQL types
|
||
|
|
|
||
|
|
**Dependencies:** Task 11, Task 12
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 14: Implement Mutation resolvers
|
||
|
|
|
||
|
|
**Objective:** Create GraphQL mutation resolvers for modifying members
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. In `src/resolvers/member.py`, define Mutation class
|
||
|
|
2. Implement createMember(input: CreateMemberInput) -> Member
|
||
|
|
3. Implement updateMember(input: UpdateMemberInput) -> Member
|
||
|
|
4. Implement deleteMember(id: ID) -> bool
|
||
|
|
5. Add validation calls in createMember and updateMember
|
||
|
|
6. Implement error handling for not found cases
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] createMember validates email and phone
|
||
|
|
- [ ] updateMember allows partial updates
|
||
|
|
- [ ] deleteMember raises error for non-existent ID
|
||
|
|
- [ ] All mutations use database session correctly
|
||
|
|
|
||
|
|
**Dependencies:** Task 13
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 15: Integrate GraphQL with FastAPI
|
||
|
|
|
||
|
|
**Objective:** Mount Strawberry schema on FastAPI app
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. In `src/main.py`, import FastAPI and Strawberry components
|
||
|
|
2. Create FastAPI app instance
|
||
|
|
3. Create Strawberry schema from Query and Mutation
|
||
|
|
4. Create GraphQLRouter with schema and graphiql=True
|
||
|
|
5. Mount router at app.add_route("/graphql", ...)
|
||
|
|
6. Add startup event to test database connection
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] `uv run uvicorn src.main:app --reload` starts server
|
||
|
|
- [ ] http://localhost:8000/graphql shows GraphiQL
|
||
|
|
- [ ] Schema introspection shows Member type and operations
|
||
|
|
|
||
|
|
**Dependencies:** Task 14
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 4: Testing and Sample Data
|
||
|
|
|
||
|
|
### Task 16: Create test infrastructure
|
||
|
|
|
||
|
|
**Objective:** Set up pytest fixtures for database testing
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Create `tests/` directory with `__init__.py`
|
||
|
|
2. Create `tests/conftest.py` with fixtures
|
||
|
|
3. Implement async_db_session fixture using in-memory SQLite
|
||
|
|
4. Implement test database setup/teardown
|
||
|
|
5. Configure pytest-asyncio in pyproject.toml
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] `uv run pytest --collect-only` finds tests directory
|
||
|
|
- [ ] Fixtures can be imported by test files
|
||
|
|
- [ ] Test database is isolated from development database
|
||
|
|
|
||
|
|
**Dependencies:** Task 3, Task 10
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 17: Write tests for CRUD operations
|
||
|
|
|
||
|
|
**Objective:** Verify all member operations work correctly including minimal creation
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Create `tests/test_member_crud.py`
|
||
|
|
2. Write test_create_member_success (with all fields)
|
||
|
|
3. Write test_create_member_minimal (only firstName)
|
||
|
|
4. Write test_create_member_invalid_email (conditional validation)
|
||
|
|
5. Write test_create_member_no_validation_when_null (email=None doesn't validate)
|
||
|
|
6. Write test_get_member_by_id
|
||
|
|
7. Write test_list_members_sorted (including members with null lastName)
|
||
|
|
8. Write test_update_member_partial
|
||
|
|
9. Write test_delete_member
|
||
|
|
10. Write test_validation_errors
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] `uv run pytest` runs all tests
|
||
|
|
- [ ] All tests pass including minimal member creation
|
||
|
|
- [ ] Conditional validation tests pass (null fields skip validation)
|
||
|
|
- [ ] Coverage for resolvers is >80%
|
||
|
|
|
||
|
|
**Dependencies:** Task 16, Task 15
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 18: Create database seed script
|
||
|
|
|
||
|
|
**Objective:** Implement idempotent script to create sample member with partial data
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Create `scripts/` directory
|
||
|
|
2. Create `scripts/seed.py` with async main function
|
||
|
|
3. Check if member with email "jane.doe@example.com" exists
|
||
|
|
4. If not, create sample member with firstName, lastName, email (address and phone fields null)
|
||
|
|
5. Demonstrates that only firstName is required, other fields optional
|
||
|
|
6. Add asyncio.run() to execute main()
|
||
|
|
7. Make script executable and add shebang
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] `uv run python scripts/seed.py` creates sample member
|
||
|
|
- [ ] Sample member has firstName, lastName, email populated
|
||
|
|
- [ ] Sample member has null values for address and phone fields
|
||
|
|
- [ ] Running script twice doesn't create duplicate
|
||
|
|
- [ ] Sample member is queryable via GraphQL
|
||
|
|
- [ ] Script prints confirmation message
|
||
|
|
|
||
|
|
**Dependencies:** Task 10, Task 15
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Phase 5: Validation and Documentation
|
||
|
|
|
||
|
|
### Task 19: Run code quality checks
|
||
|
|
|
||
|
|
**Objective:** Ensure all code meets formatting and linting standards
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Run `uv run black .` to format all code
|
||
|
|
2. Run `uv run isort .` to sort imports
|
||
|
|
3. Run `uv run ruff check .` to verify linting
|
||
|
|
4. Fix any reported issues
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] `uv run black --check .` exits with code 0
|
||
|
|
- [ ] `uv run ruff check .` reports no errors
|
||
|
|
- [ ] All Python files are consistently formatted
|
||
|
|
|
||
|
|
**Dependencies:** Task 4, Task 18
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Task 20: Verify end-to-end functionality
|
||
|
|
|
||
|
|
**Objective:** Test complete user workflow via GraphQL playground
|
||
|
|
|
||
|
|
**Steps:**
|
||
|
|
1. Start server with `uv run uvicorn src.main:app --reload`
|
||
|
|
2. Open http://localhost:8000/graphql
|
||
|
|
3. Execute createMember mutation with valid data
|
||
|
|
4. Execute members query to list all members
|
||
|
|
5. Execute updateMember mutation to change email
|
||
|
|
6. Execute deleteMember mutation to remove member
|
||
|
|
7. Verify error handling with invalid inputs
|
||
|
|
|
||
|
|
**Validation:**
|
||
|
|
- [ ] All GraphQL operations work via playground
|
||
|
|
- [ ] Sample member exists after seed script
|
||
|
|
- [ ] Error messages are clear and helpful
|
||
|
|
- [ ] Database persists data between server restarts
|
||
|
|
|
||
|
|
**Dependencies:** Task 18, Task 19
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Summary
|
||
|
|
|
||
|
|
**Total tasks:** 20
|
||
|
|
**Parallel opportunities:**
|
||
|
|
- Tasks 2, 3, 5 can start together after Task 1
|
||
|
|
- Task 11 can run in parallel with Tasks 6-10
|
||
|
|
- Tasks 16-18 can run in parallel
|
||
|
|
|
||
|
|
**Critical path:** Task 1 → Task 2 → Task 6 → Task 7 → Task 8 → Task 9 → Task 10 → Task 15 → Task 20
|
||
|
|
|
||
|
|
**Estimated completion:** Tasks build incrementally; each phase validates previous work.
|
||
|
|
|
||
|
|
**Verification:** After Task 20, run:
|
||
|
|
```bash
|
||
|
|
uv run pytest # All tests pass
|
||
|
|
uv run alembic upgrade head # Database schema current
|
||
|
|
uv run python scripts/seed.py # Sample data loaded
|
||
|
|
uv run uvicorn src.main:app --reload # Server starts
|
||
|
|
# Open http://localhost:8000/graphql # GraphiQL works
|
||
|
|
```
|