Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 172 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Generates GraphQL types, inputs, queries and resolvers directly from SQLAlchemy
- [Mapping SQLAlchemy Models](#mapping-sqlalchemy-models)
- [Resolver Generation](#resolver-generation)
- [Pagination](#pagination)
- [Ordering](#ordering)
- [Filtering](#filtering)
- [Aggregations](#aggregations)
- [Mutations](#mutations)
Expand Down Expand Up @@ -567,20 +568,25 @@ def farms(self) -> str:
Strawchemy supports offset-based pagination out of the box.

<details>
<summary>Pagination example:</summary>
<summary>Pagination examples</summary>

Enable pagination on fields:
### Field-Level Pagination

Enable pagination on specific fields:

```python
from strawchemy.schema.pagination import DefaultOffsetPagination


@strawberry.type
class Query:
# Enable pagination with default settings
# Enable pagination with default settings (limit=100, offset=0)
users: list[UserType] = strawchemy.field(pagination=True)
# Customize pagination defaults
users_custom_pagination: list[UserType] = strawchemy.field(pagination=DefaultOffsetPagination(limit=20))

# Customize pagination defaults for this specific field
users_custom: list[UserType] = strawchemy.field(
pagination=DefaultOffsetPagination(limit=20, offset=10)
)
```

In your GraphQL queries, you can use the `offset` and `limit` parameters:
Expand All @@ -594,10 +600,40 @@ In your GraphQL queries, you can use the `offset` and `limit` parameters:
}
```

You can also enable pagination for nested relationships:
### Config-Level Pagination

Enable pagination globally for all list fields:

```python
@strawchemy.type(User, include="all", child_pagination=True)
from strawchemy import Strawchemy, StrawchemyConfig

strawchemy = Strawchemy(
StrawchemyConfig(
"postgresql",
pagination="all", # Enable on all list fields
pagination_default_limit=100, # Default limit
pagination_default_offset=0, # Default offset
)
)


@strawchemy.type(User, include="all")
class UserType:
pass


@strawberry.type
class Query:
# This field automatically has pagination enabled
users: list[UserType] = strawchemy.field()
```

### Type-level pagination

Enable pagination for nested relationships from a specific type:

```python
@strawchemy.type(User, include="all", paginate="all")
class UserType:
pass
```
Expand All @@ -619,6 +655,118 @@ Then in your GraphQL queries:

</details>

## Ordering

Strawchemy provides flexible ordering capabilities for query results.

<details>
<summary>Ordering examples</summary>

### Field-Level Ordering

Define ordering inputs and use them on specific fields:

```python
# Create order by input
@strawchemy.order(User, include="all")
class UserOrderBy:
pass


@strawberry.type
class Query:
users: list[UserType] = strawchemy.field(order_by=UserOrderBy)
```

Query with ordering:

```graphql
{
users(orderBy: [{ name: ASC }, { createdAt: DESC }]) {
id
name
createdAt
}
}
```

Available ordering options:

- `ASC` - Ascending order
- `DESC` - Descending order
- `ASC_NULLS_FIRST` - Ascending with nulls first
- `ASC_NULLS_LAST` - Ascending with nulls last
- `DESC_NULLS_FIRST` - Descending with nulls first
- `DESC_NULLS_LAST` - Descending with nulls last

### Type-Level Ordering

Enable ordering automatically on a type:

```python
@strawchemy.type(User, include="all", order="all")
class UserType:
pass
```

This automatically generates and applies an order by input for all fields using this type.

### Config-Level Ordering

Enable ordering globally for all list fields:

```python
from strawchemy import Strawchemy, StrawchemyConfig

strawchemy = Strawchemy(
StrawchemyConfig(
"postgresql",
order_by="all", # Enable ordering on all list fields
)
)


@strawchemy.type(User, include="all")
class UserType:
pass


@strawberry.type
class Query:
# This field automatically has ordering enabled
users: list[UserType] = strawchemy.field()
```

With this configuration, all list fields will automatically have an `orderBy` argument without needing to specify it per
field.

### Nested Relationship Ordering

Order nested relationships:

```python
@strawchemy.type(User, include="all", order="all")
class UserType:
pass
```

Query with nested ordering:

```graphql
{
users(orderBy: [{ name: ASC }]) {
id
name
posts(orderBy: [{ title: ASC }]) {
id
title
}
}
}
```

</details>

## Filtering

Strawchemy provides powerful filtering capabilities.
Expand Down Expand Up @@ -1950,18 +2098,20 @@ Configuration is made by passing a `StrawchemyConfig` to the `Strawchemy` instan

### Configuration Options

| Option | Type | Default | Description |
|----------------------------|-------------------------------------------------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `dialect` | `SupportedDialect` | | Database dialect to use. Supported dialects are "postgresql", "mysql", "sqlite". |
| `session_getter` | `Callable[[Info], Session]` | `default_session_getter` | Function to retrieve SQLAlchemy session from strawberry `Info` object. By default, it retrieves the session from `info.context.session`. |
| `auto_snake_case` | `bool` | `True` | Automatically convert snake cased names to camel case in GraphQL schema. |
| `repository_type` | `type[Repository] \| StrawchemySyncRepository` | `StrawchemySyncRepository` | Repository class to use for auto resolvers. |
| `filter_overrides` | `OrderedDict[tuple[type, ...], type[SQLAlchemyFilterBase]]` | `None` | Override default filters with custom filters. This allows you to provide custom filter implementations for specific column types. |
| `execution_options` | `dict[str, Any]` | `None` | SQLAlchemy execution options for repository operations. These options are passed to the SQLAlchemy `execution_options()` method. |
| `pagination_default_limit` | `int` | `100` | Default pagination limit when `pagination=True`. |
| `pagination` | `bool` | `False` | Enable/disable pagination on list resolvers by default. |
| `default_id_field_name` | `str` | `"id"` | Name for primary key fields arguments on primary key resolvers. |
| `deterministic_ordering` | `bool` | `True` | Force deterministic ordering for list resolvers. |
| Option | Type | Default | Description |
|-----------------------------|-------------------------------------------------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `dialect` | `SupportedDialect` | | Database dialect to use. Supported dialects are "postgresql", "mysql", "sqlite". |
| `session_getter` | `Callable[[Info], Session]` | `default_session_getter` | Function to retrieve SQLAlchemy session from strawberry `Info` object. By default, it retrieves the session from `info.context.session`. |
| `auto_snake_case` | `bool` | `True` | Automatically convert snake cased names to camel case in GraphQL schema. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix hyphenation in configuration table.

"snake cased names" should be hyphenated as "snake-cased names" for proper English grammar.

🔎 Proposed fix
-| `auto_snake_case`           | `bool`                                                      | `True`                     | Automatically convert snake cased names to camel case in GraphQL schema.                                                                 |
+| `auto_snake_case`           | `bool`                                                      | `True`                     | Automatically convert snake-cased names to camel case in GraphQL schema.                                                                 |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `auto_snake_case` | `bool` | `True` | Automatically convert snake cased names to camel case in GraphQL schema. |
| `auto_snake_case` | `bool` | `True` | Automatically convert snake-cased names to camel case in GraphQL schema. |
🧰 Tools
🪛 LanguageTool

[grammar] ~2105-~2105: Use a hyphen to join words.
Context: ... | Automatically convert snake cased names to camel case in GraphQL sch...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
In README.md around line 2105, the phrase "snake cased names" in the
configuration table should be corrected to "snake-cased names"; update the table
cell text to use the hyphenated form "snake-cased names" so the grammar is
correct while preserving the rest of the sentence and formatting.

| `repository_type` | `type[Repository] \| StrawchemySyncRepository` | `StrawchemySyncRepository` | Repository class to use for auto resolvers. |
| `filter_overrides` | `OrderedDict[tuple[type, ...], type[SQLAlchemyFilterBase]]` | `None` | Override default filters with custom filters. This allows you to provide custom filter implementations for specific column types. |
| `execution_options` | `dict[str, Any]` | `None` | SQLAlchemy execution options for repository operations. These options are passed to the SQLAlchemy `execution_options()` method. |
| `default_id_field_name` | `str` | `"id"` | Name for primary key fields arguments on primary key resolvers. |
| `deterministic_ordering` | `bool` | `True` | Force deterministic ordering for list resolvers. |
| `pagination` | `Literal["all"] \| None` | `None` | Enable/disable pagination on list resolvers by default. Set to `"all"` to enable pagination on all list fields. |
| `order_by` | `Literal["all"] \| None` | `None` | Enable/disable order by on list resolvers by default. Set to `"all"` to enable ordering on all list fields. |
| `pagination_default_limit` | `int` | `100` | Default pagination limit when `pagination=True`. |
| `pagination_default_offset` | `int` | `0` | Default pagination offset when `pagination=True`. |

### Example

Expand All @@ -1980,8 +2130,10 @@ strawchemy = Strawchemy(
"postgresql",
session_getter=get_session_from_context,
auto_snake_case=True,
pagination=True,
pagination="all",
pagination_default_limit=50,
pagination_default_offset=0,
order_by="all",
default_id_field_name="pk",
)
)
Expand Down
8 changes: 4 additions & 4 deletions examples/testapp/testapp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from strawchemy import Strawchemy, StrawchemyAsyncRepository, StrawchemyConfig
from testapp.models import Customer, Milestone, Project, Ticket

strawchemy = Strawchemy(StrawchemyConfig("sqlite", repository_type=StrawchemyAsyncRepository))
strawchemy = Strawchemy(StrawchemyConfig("sqlite", repository_type=StrawchemyAsyncRepository, include=["name"]))

# Ticket

Expand All @@ -20,7 +20,7 @@ class TicketOrder: ...
class TicketFilter: ...


@strawchemy.type(Ticket, include="all", filter_input=TicketFilter, order_by=TicketOrder, override=True)
@strawchemy.type(Ticket, include="all", filter_input=TicketFilter, order=TicketOrder, override=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for order_by usage in strawchemy.type() and strawchemy.field() calls
echo "=== Type decorator usage with order_by ==="
rg -nP '@strawchemy\.type\([^)]*order_by\s*=' --type=py

echo -e "\n=== Field usage with order_by ==="
rg -nP 'strawchemy\.field\([^)]*order_by\s*=' --type=py

echo -e "\n=== Type decorator usage with order= (new pattern) ==="
rg -nP '@strawchemy\.type\([^)]*order\s*=' --type=py | head -20

echo -e "\n=== Field usage with order= (new pattern) ==="
rg -nP 'strawchemy\.field\([^)]*order\s*=' --type=py | head -20

Repository: gazorby/strawchemy

Length of output: 3173


Update field-level API to use order= parameter for consistency.

The type decorator has been migrated to use order=, but strawchemy.field() calls still use order_by=. This creates an API inconsistency that needs to be resolved:

  • Type-level: Uses order= (e.g., line 23 of examples/testapp/testapp/types.py)
  • Field-level: Still uses order_by= (found in 7 test files including tests/unit/schemas/order/field_order_by_specific_fields.py:18)

Update all field-level calls to use order= to match the type-level API.

🤖 Prompt for AI Agents
In @examples/testapp/testapp/types.py at line 23, The project mixes two ordering
parameters: the type decorator uses order= (e.g., @strawchemy.type(Ticket,
include="all", filter_input=TicketFilter, order=TicketOrder, override=True))
while field-level calls still use order_by=; update all strawchemy.field(...)
usages to replace order_by= with order= so the field-level API matches the
type-level API (search for strawchemy.field in tests and examples, e.g.,
tests/unit/schemas/order/field_order_by_specific_fields.py and
examples/testapp/testapp/types.py references to
Ticket/TicketFilter/TicketOrder).

class TicketType: ...


Expand Down Expand Up @@ -55,7 +55,7 @@ class ProjectOrder: ...
class ProjectFilter: ...


@strawchemy.type(Project, include="all", filter_input=ProjectFilter, order_by=ProjectOrder, override=True)
@strawchemy.type(Project, include="all", filter_input=ProjectFilter, order=ProjectOrder, override=True)
class ProjectType: ...


Expand All @@ -66,7 +66,7 @@ class ProjectCreate: ...
# Milestone


@strawchemy.type(Milestone, include="all", override=True)
@strawchemy.type(Milestone, include={"name"}, override=True, distinct_on=["age"], paginate=["projects"])
class MilestoneType: ...
Comment on lines +69 to 70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

paginate=["projects"] is dead config with the current include set.

include={"name"} prevents projects from being generated on MilestoneType, so this pagination option can never appear in the schema. Include projects here or drop the pagination setting from the example.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/testapp/testapp/types.py` around lines 69 - 70, The decorator on
MilestoneType uses include={"name"} which prevents generating the related field
projects, so paginate=["projects"] is unreachable; either add "projects" to the
include set in the `@strawchemy.type`(...) call (so projects is generated and
pagination works) or remove paginate=["projects"] from the decorator; update the
`@strawchemy.type` decorator on MilestoneType accordingly.



Expand Down
32 changes: 28 additions & 4 deletions src/strawchemy/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from __future__ import annotations

from dataclasses import dataclass, field
from functools import cached_property
from typing import TYPE_CHECKING, Any

from strawchemy.dto.inspectors import SQLAlchemyGraphQLInspector
from strawchemy.dto.types import DTOConfig, FieldIterable, IncludeFields
from strawchemy.repository.strawberry import StrawchemySyncRepository
from strawchemy.utils.strawberry import default_session_getter

Expand Down Expand Up @@ -43,17 +45,39 @@ class StrawchemyConfig:
"""Override default filters with custom filters."""
execution_options: dict[str, Any] | None = None
"""SQLAlchemy execution options for strawberry operations."""
pagination_default_limit: int = 100
"""Default pagination limit when `pagination=True`."""
pagination: bool = False
"""Enable/disable pagination on list resolvers."""
default_id_field_name: str = "id"
"""Name for primary key fields arguments on primary key resolvers."""
deterministic_ordering: bool = True
"""Force deterministic ordering for list resolvers."""
include: IncludeFields = "all"
"""Globally included fields."""
exclude: FieldIterable | None = None
"""Globally included fields."""
pagination: IncludeFields | None = None
"""Enable/disable pagination on list resolvers."""
order_by: IncludeFields | None = None
"""Enable/disable order by on list resolvers."""
distinct_on: IncludeFields | None = None
"""Enable/disable order by onelist resolvers."""
pagination_default_limit: int = 100
"""Default pagination limit when `pagination=True`."""
pagination_default_offset: int = 0
"""Default pagination offset when `pagination=True`."""
Comment on lines +62 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inline docstrings reference outdated API.

The docstrings on lines 55 and 57 mention pagination=True, but the new API uses pagination="all".

🔎 Proposed fix
     pagination_default_limit: int = 100
-    """Default pagination limit when `pagination=True`."""
+    """Default pagination limit when pagination is enabled."""
     pagination_default_offset: int = 0
-    """Default pagination offset when `pagination=True`."""
+    """Default pagination offset when pagination is enabled."""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pagination_default_limit: int = 100
"""Default pagination limit when `pagination=True`."""
pagination_default_offset: int = 0
"""Default pagination offset when `pagination=True`."""
pagination_default_limit: int = 100
"""Default pagination limit when pagination is enabled."""
pagination_default_offset: int = 0
"""Default pagination offset when pagination is enabled."""
🤖 Prompt for AI Agents
In src/strawchemy/config/base.py around lines 54 to 57, the inline docstrings
reference the outdated API value `pagination=True`; update both docstrings to
reference the new API value `pagination="all"` (e.g., change `when
`pagination=True`` to `when `pagination="all"``) so they accurately reflect the
current parameter semantics and quoting style.

Comment on lines +52 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix documentation errors in field docstrings.

There are two issues in the docstrings:

  1. Line 54: The exclude field docstring says "Globally included fields" but should describe excluded fields
  2. Line 60: Typo "onelist" should be "on list"
📝 Proposed fix
     include: IncludeFields = "all"
     """Globally included fields."""
     exclude: FieldIterable | None = None
-    """Globally included fields."""
+    """Globally excluded fields."""
     pagination: IncludeFields | None = None
     """Enable/disable pagination on list resolvers."""
     order_by: IncludeFields | None = None
     """Enable/disable order by on list resolvers."""
     distinct_on: IncludeFields | None = None
-    """Enable/disable order by onelist resolvers."""
+    """Enable/disable distinct on for list resolvers."""
     pagination_default_limit: int = 100
     """Default pagination limit when `pagination=True`."""
     pagination_default_offset: int = 0
     """Default pagination offset when `pagination=True`."""
🤖 Prompt for AI Agents
In @src/strawchemy/config/base.py around lines 51 - 64, Update the field
docstrings to correct their descriptions: change the docstring for the exclude
field (symbol: exclude: FieldIterable | None) from "Globally included fields."
to something like "Globally excluded fields." and fix the typo in the
distinct_on field docstring (symbol: distinct_on: IncludeFields | None) from
"Enable/disable order by onelist resolvers." to "Enable/disable order by on list
resolvers."


inspector: SQLAlchemyGraphQLInspector = field(init=False)

def __post_init__(self) -> None:
"""Initializes the SQLAlchemyGraphQLInspector after the dataclass is created."""
self.inspector = SQLAlchemyGraphQLInspector(self.dialect, filter_overrides=self.filter_overrides)

@cached_property
def order_config(self) -> DTOConfig:
return DTOConfig.from_include(self.order_by)

@cached_property
def distinct_on_config(self) -> DTOConfig:
return DTOConfig.from_include(self.distinct_on)

@cached_property
def pagination_config(self) -> DTOConfig:
return DTOConfig.from_include(self.pagination)
Comment on lines +73 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Avoid memoizing derived configs on a mutable dataclass.

StrawchemyConfig is still writable after construction, so once one of these accessors is read, later changes to pagination, order_by, or distinct_on are ignored silently. Either freeze the config or make these plain properties instead of cached_property.

♻️ Minimal fix
-    `@cached_property`
+    `@property`
     def order_config(self) -> DTOConfig:
         return DTOConfig.from_include(self.order_by)

-    `@cached_property`
+    `@property`
     def distinct_on_config(self) -> DTOConfig:
         return DTOConfig.from_include(self.distinct_on)

-    `@cached_property`
+    `@property`
     def pagination_config(self) -> DTOConfig:
         return DTOConfig.from_include(self.pagination)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@cached_property
def order_config(self) -> DTOConfig:
return DTOConfig.from_include(self.order_by)
@cached_property
def distinct_on_config(self) -> DTOConfig:
return DTOConfig.from_include(self.distinct_on)
@cached_property
def pagination_config(self) -> DTOConfig:
return DTOConfig.from_include(self.pagination)
`@property`
def order_config(self) -> DTOConfig:
return DTOConfig.from_include(self.order_by)
`@property`
def distinct_on_config(self) -> DTOConfig:
return DTOConfig.from_include(self.distinct_on)
`@property`
def pagination_config(self) -> DTOConfig:
return DTOConfig.from_include(self.pagination)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/strawchemy/config/base.py` around lines 73 - 83, The cached_property
usage causes stale derived configs when StrawchemyConfig is mutated; change
order_config, distinct_on_config, and pagination_config to compute on each
access (replace `@cached_property` with plain `@property` or remove the caching) so
DTOConfig.from_include is re-evaluated against the current pagination, order_by,
and distinct_on values, or alternatively make the StrawchemyConfig dataclass
frozen/immutable so memoization is safe; update the decorators on the methods
(order_config, distinct_on_config, pagination_config) accordingly and ensure
callers still use the same attribute names.

Loading
Loading