2025-11-20 13:26:43 +01:00
|
|
|
# graphql-api Specification
|
2025-11-20 11:29:06 +01:00
|
|
|
|
2025-11-20 13:26:43 +01:00
|
|
|
## Purpose
|
|
|
|
|
TBD - created by archiving change add-graphql-member-api. Update Purpose after archive.
|
|
|
|
|
## Requirements
|
2025-11-20 11:29:06 +01:00
|
|
|
### 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
|
|
|
|
|
|
2025-11-21 12:08:58 +01:00
|
|
|
### Requirement: GraphQL Introspection Support
|
|
|
|
|
The GraphQL API SHALL support introspection queries that enable clients to discover the complete schema dynamically.
|
|
|
|
|
|
|
|
|
|
#### Scenario: Introspection query returns schema types
|
|
|
|
|
- **GIVEN** the GraphQL API is running
|
|
|
|
|
- **WHEN** a client executes an introspection query for `__schema`
|
|
|
|
|
- **THEN** the response SHALL include all types defined in the schema
|
|
|
|
|
- **AND** include queries, mutations, and object types
|
|
|
|
|
- **AND** include field names, types, and descriptions
|
|
|
|
|
|
|
|
|
|
#### Scenario: Introspection query returns type details
|
|
|
|
|
- **GIVEN** the GraphQL API is running
|
|
|
|
|
- **WHEN** a client executes an introspection query for `__type(name: "Member")`
|
|
|
|
|
- **THEN** the response SHALL include complete Member type details
|
|
|
|
|
- **AND** include all fields with their types
|
|
|
|
|
- **AND** include field descriptions where defined
|
|
|
|
|
|
|
|
|
|
#### Scenario: Introspection is enabled by default
|
|
|
|
|
- **GIVEN** the Strawberry GraphQL schema is configured
|
|
|
|
|
- **WHEN** the schema is created
|
|
|
|
|
- **THEN** introspection SHALL be enabled
|
|
|
|
|
- **AND** the `__schema` and `__type` queries SHALL be available
|
|
|
|
|
|