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 key

  • foreign_key: str - Reference another table (e.g., “users.id”)

  • index: bool - Create an index on this column

  • unique: bool - Add unique constraint

  • nullable: bool - Allow NULL values

  • db_column: str - Custom column name

  • where: str - SQL WHERE clause for relations

  • args: str | list[str] - Arguments to pass to relation query

  • relation: 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 definitions

  • sql.py - SQLAlchemy models

  • context.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:

  1. 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]
    }
    
  2. 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:

  1. Only fetching related data when requested in the GraphQL query

  2. Applying filters at the database level

  3. Using parent object fields efficiently in relationship queries

  4. 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