Files
clubber/openspec/changes/add-graphql-member-api/tasks.md
T
gurixandClaude f3f5e0282c feat: add OpenSpec proposal for GraphQL member API
Create comprehensive OpenSpec proposal for implementing a minimal GraphQL API
for member management with flexible validation requirements.

Key features:
- Only firstName required for member creation
- Conditional validation (email/phone format validated only when provided)
- Full CRUD operations via GraphQL
- SQLite database with Alembic migrations
- FastAPI + Strawberry GraphQL integration

Includes 4 capability specs:
- project-setup: Python project structure with uv dependency management
- database-layer: SQLAlchemy async models with nullable fields
- graphql-api: Strawberry schema with minimal required fields
- member-crud: Business logic with conditional validation

Implementation broken down into 20 discrete tasks across 5 phases.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 11:29:06 +01:00

14 KiB

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:

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