Code Generation#
The cannula codegen command generates Python code from your GraphQL schema. It creates type definitions, SQLAlchemy models, and context classes based on your schema and metadata.
See the tutorial for an example Part3: Code Generation
Configuration#
By default, the command looks for a pyproject.toml file in the current directory. Here’s a sample configuration:
[tool.cannula.codegen]
schema = "schema/" # Directory containing .graphql files
output = "generated/" # Output directory for generated code
use_pydantic = false # Use pydantic models instead of dataclasses
Schema Metadata#
You can add metadata to your GraphQL schema using descriptions and directives. Here are the supported options:
Type Metadata#
Add metadata to types using descriptions with YAML frontmatter:
"""
User type
---
metadata:
db_table: users # SQLAlchemy table name
cache: false # Disable caching for this type
ttl: 0 # Cache TTL
weight: 1.2 # Custom metadata
"""
type User {
id: ID!
name: String!
}
Field Metadata#
Add metadata to fields using directives or descriptions:
type User {
"User ID @metadata(primary_key: true)"
id: ID!
"@metadata(index: true)"
name: String!
"@metadata(db_column: email_address, unique: true)"
email: String!
"@metadata(nullable: true)"
age: Int
"""
User's projects
---
metadata:
where: "author_id = :id"
args: id
"""
projects(limit: Int = 10): [Project]
}
Supported Field Metadata#
primary_key: bool- Mark field as primary keyforeign_key: str- Reference another table (e.g., “users.id”)index: bool- Create an index on this columnunique: bool- Add unique constraintnullable: bool- Allow NULL valuesdb_column: str- Custom column namewhere: str- SQL WHERE clause for relationsargs: str | list[str]- Arguments to pass to relation queryrelation: dict- SQLAlchemy relationship options
Relationships#
Define relationships between types:
type User {
id: ID!
"@metadata(foreign_key: projects.id)"
project_id: String!
"""
User's project
---
metadata:
relation:
back_populates: "author"
cascade: "all, delete-orphan"
"""
project: Project!
}
Generated Code#
The command generates three files:
types.py- Python type definitionssql.py- SQLAlchemy modelscontext.py- Context classes with data sources
Example#
Here’s a complete example:
"""
User in the system
---
metadata:
db_table: users
"""
type User {
"User ID @metadata(primary_key: true)"
id: ID!
"@metadata(index: true)"
name: String!
"@metadata(db_column: email_address, unique: true)"
email: String!
"""
User's projects
---
metadata:
where: "author_id = :id"
args: id
"""
projects: [Project]
}
"""
Project type
---
metadata:
db_table: projects
"""
type Project {
"Project ID @metadata(primary_key: true)"
id: ID!
name: String!
"@metadata(foreign_key: users.id)"
author_id: ID!
author: User!
}
This will generate:
SQLAlchemy models with proper relationships
Python types with computed fields
A context class with User and Project datasources
Relationship Queries#
When defining relationships between types, you can specify how to fetch related data using where clauses and arguments. This is especially useful for filtering relationships and optimizing queries.
Where Clauses#
The where clause in metadata defines the SQL condition for fetching related data. It uses SQLAlchemy text syntax with named parameters:
---
metadata:
where: "author_id = :id"
args: id
Arguments#
There are two types of arguments you can use in relationship queries:
Metadata Arguments (
args) These reference fields from the parent type that are passed to the where clause:type User { id: ID! org_id: ID! """ Projects in user's organization --- metadata: where: "org_id = :org_id AND author_id = :id" args: [id, org_id] """ projects: [Project] }
Field Arguments These are regular GraphQL arguments that can be used in queries:
type User { id: ID! """ User's projects with filtering --- metadata: where: "author_id = :id AND is_active = :active" args: id """ projects(active: Boolean = true): [Project] }
Combining Arguments#
You can combine both types of arguments:
type User {
id: ID!
org_id: ID!
"""
Filtered projects
---
metadata:
where: "org_id = :org_id AND author_id = :id AND created_at > :since"
args: [id, org_id]
"""
projects(since: DateTime!): [Project]
}
In this example:
- id and org_id come from the User object
- since comes from the GraphQL query argument
Query Example:
query {
user {
id
# Fetches projects where:
# org_id = user.org_id AND
# author_id = user.id AND
# created_at > '2024-01-01'
projects(since: "2024-01-01") {
name
}
}
}
Default Values#
Field arguments can have default values:
type User {
id: ID!
"""
Active projects by default
---
metadata:
where: "author_id = :id AND is_active = :active"
args: id
"""
projects(active: Boolean = true): [Project]
}
Query Optimization#
The relationship query system helps optimize database queries by:
Only fetching related data when requested in the GraphQL query
Applying filters at the database level
Using parent object fields efficiently in relationship queries
Supporting default filters via field argument defaults
Running#
Generate code by running:
$ cannula codegen
Options:
--schema PATH- Schema directory (overrides pyproject.toml)--output PATH- Output directory (overrides pyproject.toml)--use-pydantic- Use pydantic models--dry-run- Print output without writing files