Files
clubber/openspec/changes/add-graphql-member-api/tasks.md
T

462 lines
14 KiB
Markdown
Raw Normal View History

# 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
```