Files
clubber/openspec/changes/add-graphql-member-api/specs/graphql-api/spec.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

409 lines
11 KiB
Markdown

# Spec: GraphQL API
**Capability:** graphql-api
**Status:** Draft
**Last Updated:** 2025-11-20
## Overview
This capability defines the GraphQL API layer using Strawberry GraphQL integrated with FastAPI, providing type-safe schema, queries, and mutations for member management.
## ADDED Requirements
### Requirement: Strawberry GraphQL schema with Member type
The system MUST define a GraphQL schema using Strawberry with Member object type mapping to database model fields.
**Member GraphQL type fields:**
- `id`: ID! (non-null unique identifier)
- `firstName`: String! (non-null)
- `lastName`: String (nullable)
- `street`: String (nullable)
- `apartmentNumber`: String (nullable)
- `zip`: String (nullable)
- `city`: String (nullable)
- `country`: String (nullable)
- `email`: String (nullable)
- `phone`: String (nullable)
- `createdAt`: DateTime! (non-null timestamp)
- `updatedAt`: DateTime! (non-null timestamp)
#### Scenario: Member type is defined with Strawberry decorator
**Given** src/schemas/member.py exists
**When** Member class is decorated with @strawberry.type
**Then** GraphQL type "Member" is registered in schema
**And** all fields use camelCase naming (GraphQL convention)
**And** field types match SQLAlchemy model types
**And** snake_case database fields map to camelCase GraphQL fields
#### Scenario: GraphQL schema is introspectable
**Given** FastAPI app with GraphQL endpoint is running
**When** client accesses /graphql endpoint
**Then** GraphiQL playground is displayed
**And** schema introspection reveals Member type
**And** all fields and their types are documented
### Requirement: Input types for mutations
The system MUST define separate input types for create and update operations with appropriate field requirements.
**CreateMemberInput fields (only firstName required):**
- `firstName`: String!
- `lastName`: String
- `street`: String
- `apartmentNumber`: String
- `zip`: String
- `city`: String
- `country`: String
- `email`: String
- `phone`: String
**UpdateMemberInput fields (id required, all others optional):**
- `id`: ID!
- `firstName`: String
- `lastName`: String
- `street`: String
- `apartmentNumber`: String
- `zip`: String
- `city`: String
- `country`: String
- `email`: String
- `phone`: String
#### Scenario: CreateMemberInput enforces required fields
**Given** CreateMemberInput type is defined
**When** client sends mutation without firstName
**Then** GraphQL validation error is returned
**And** error message indicates firstName is required
**When** client sends mutation with only firstName
**Then** input validation passes
**And** member is created with only firstName populated
#### Scenario: UpdateMemberInput allows partial updates
**Given** UpdateMemberInput type is defined
**When** client sends mutation with only id and email
**Then** only email field is updated in database
**And** all other fields remain unchanged
**When** client sends mutation without id
**Then** GraphQL validation error is returned
### Requirement: Query resolvers for reading members
The system MUST provide GraphQL query resolvers for fetching individual members and listing all members.
**Query operations:**
- `member(id: ID!): Member` - Get single member by ID
- `members: [Member!]!` - List all members
#### Scenario: Query single member by ID
**Given** member exists with id=1
**When** client executes query:
```graphql
query {
member(id: 1) {
id
firstName
lastName
email
}
}
```
**Then** response contains member data
**And** response matches GraphQL Member type structure
#### Scenario: Query member with non-existent ID
**Given** no member exists with id=999
**When** client executes query `member(id: 999)`
**Then** response returns null for member field
**And** no error is raised (null is valid for nullable return)
#### Scenario: List all members
**Given** multiple members exist in database
**When** client executes query:
```graphql
query {
members {
id
firstName
lastName
}
}
```
**Then** response contains array of all members
**And** members are sorted by last name, first name
**When** no members exist
**Then** response contains empty array
### Requirement: Mutation resolvers for modifying members
The system MUST provide GraphQL mutation resolvers for creating, updating, and deleting members.
**Mutation operations:**
- `createMember(input: CreateMemberInput!): Member!` - Create new member
- `updateMember(input: UpdateMemberInput!): Member!` - Update existing member
- `deleteMember(id: ID!): Boolean!` - Delete member
#### Scenario: Create new member mutation
**Given** valid CreateMemberInput is provided
**When** client executes mutation:
```graphql
mutation {
createMember(input: {
firstName: "Jane"
lastName: "Smith"
street: "123 Main St"
zip: "12345"
city: "Springfield"
country: "USA"
email: "jane@example.com"
phone: "+15551234567"
}) {
id
firstName
email
}
}
```
**Then** new member is persisted to database
**And** response contains newly created member with generated id
**And** createdAt and updatedAt are populated
#### Scenario: Create member with minimal data (firstName only)
**Given** CreateMemberInput with only firstName is provided
**When** client executes mutation:
```graphql
mutation {
createMember(input: {
firstName: "John"
}) {
id
firstName
lastName
email
}
}
```
**Then** new member is created in database
**And** firstName is "John"
**And** lastName, email, phone, and address fields are null
**And** response contains member with null optional fields
#### Scenario: Create member with invalid input
**Given** CreateMemberInput has invalid email format
**When** client executes createMember mutation
**Then** GraphQL error is returned
**And** error message indicates validation failure
**And** no database record is created
#### Scenario: Update existing member mutation
**Given** member exists with id=1
**When** client executes mutation:
```graphql
mutation {
updateMember(input: {
id: 1
email: "newemail@example.com"
phone: "+15559876543"
}) {
id
email
phone
updatedAt
}
}
```
**Then** member email and phone are updated in database
**And** updatedAt timestamp is refreshed
**And** all other fields remain unchanged
#### Scenario: Update non-existent member
**Given** no member exists with id=999
**When** client executes updateMember with id=999
**Then** GraphQL error is returned
**And** error message indicates "Member not found"
#### Scenario: Delete member mutation
**Given** member exists with id=1
**When** client executes mutation:
```graphql
mutation {
deleteMember(id: 1)
}
```
**Then** member is removed from database
**And** response returns true
**When** query attempts to fetch deleted member
**Then** response returns null
#### Scenario: Delete non-existent member
**Given** no member exists with id=999
**When** client executes deleteMember(id: 999)
**Then** GraphQL error is returned
**And** error message indicates "Member not found"
### Requirement: FastAPI integration with GraphQL endpoint
The system MUST integrate Strawberry GraphQL schema with FastAPI application at /graphql path with GraphiQL playground enabled.
#### Scenario: GraphQL endpoint is mounted on FastAPI app
**Given** src/main.py creates FastAPI app
**When** Strawberry schema is created from Query and Mutation classes
**Then** GraphQLRouter is created with schema
**And** router is mounted at /graphql path
**And** GraphiQL is enabled for development
#### Scenario: GraphQL playground is accessible
**Given** FastAPI app is running
**When** browser navigates to http://localhost:8000/graphql
**Then** GraphiQL interface is displayed
**And** schema documentation is available
**And** queries can be executed interactively
#### Scenario: GraphQL endpoint accepts POST requests
**Given** GraphQL endpoint is configured
**When** client sends POST to /graphql with query in body
**Then** query is executed against schema
**And** JSON response is returned with data or errors
### Requirement: Error handling with meaningful messages
The system MUST provide clear, actionable error messages for validation failures, not found errors, and server errors.
#### Scenario: Input validation error provides field-level details
**Given** createMember mutation receives invalid email
**When** mutation executes
**Then** GraphQL error includes message "Invalid email format"
**And** error path indicates which input field failed
**And** HTTP status code is 400 (Bad Request)
#### Scenario: Not found error provides resource context
**Given** member query requests non-existent id=999
**When** query executes
**Then** error message is "Member with ID 999 not found"
**And** error type indicates resource not found
#### Scenario: Database error is handled gracefully
**Given** database connection fails during query
**When** query executes
**Then** GraphQL error indicates server error
**And** internal error details are logged
**And** client receives generic "Internal server error" message
**And** HTTP status code is 500
## MODIFIED Requirements
None (new capability)
## REMOVED Requirements
None (new capability)
## Cross-References
### Depends On
- **project-setup** - Requires FastAPI and Strawberry dependencies
- **database-layer** - Requires Member model and database sessions
### Enables
- **member-crud** - Provides API interface for business operations
### Related To
None
## Implementation Notes
1. **Naming convention:** Use camelCase for GraphQL fields (firstName), snake_case for Python (first_name)
2. **Type conversion:** Strawberry automatically converts between Python and GraphQL types
3. **Async resolvers:** All resolvers must be async functions for database operations
4. **Dependency injection:** Use FastAPI's Depends() for database session in resolvers
5. **Error handling:** Raise Python exceptions, Strawberry converts to GraphQL errors
6. **GraphiQL:** Disable in production by setting `graphiql=False`
## GraphQL Schema (SDL Reference)
```graphql
type Member {
id: ID!
firstName: String!
lastName: String
street: String
apartmentNumber: String
zip: String
city: String
country: String
email: String
phone: String
createdAt: DateTime!
updatedAt: DateTime!
}
input CreateMemberInput {
firstName: String!
lastName: String
street: String
apartmentNumber: String
zip: String
city: String
country: String
email: String
phone: String
}
input UpdateMemberInput {
id: ID!
firstName: String
lastName: String
street: String
apartmentNumber: String
zip: String
city: String
country: String
email: String
phone: String
}
type Query {
member(id: ID!): Member
members: [Member!]!
}
type Mutation {
createMember(input: CreateMemberInput!): Member!
updateMember(input: UpdateMemberInput!): Member!
deleteMember(id: ID!): Boolean!
}
```
## Validation Checklist
- [ ] Member Strawberry type defined in src/schemas/member.py
- [ ] CreateMemberInput and UpdateMemberInput defined
- [ ] Query class with member and members resolvers
- [ ] Mutation class with create, update, delete resolvers
- [ ] GraphQL schema created in src/main.py
- [ ] /graphql endpoint mounted on FastAPI app
- [ ] GraphiQL accessible at http://localhost:8000/graphql
- [ ] All queries and mutations execute successfully
- [ ] Error messages are clear and actionable
- [ ] Schema introspection shows all types correctly