chore: archive add-graphql-member-api proposal and create capability specs
Archive completed GraphQL API implementation proposal: - Move proposal to openspec/changes/archive/2025-11-20-add-graphql-member-api/ - Create capability specs in openspec/specs/: - database-layer: SQLAlchemy async with Alembic migrations - graphql-api: Strawberry GraphQL with FastAPI integration - member-crud: Member management with conditional validation - project-setup: Python 3.11+ with uv package manager All 73 tasks completed and validated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user