Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ In your GraphQL queries, you can use the `offset` and `limit` parameters:
You can also enable pagination for nested relationships:

```python
@strawchemy.type(User, include="all", child_pagination=True)
@strawchemy.type(User, include="all", paginate="all")
class UserType:
pass
```
Expand Down
12 changes: 8 additions & 4 deletions src/strawchemy/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@ 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."""
pagination: bool = False
"""Enable/disable pagination on list resolvers."""
order_by: bool = False
"""Enable/disable order by on list 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 +61 to +64
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 +51 to +64
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)

Expand Down
4 changes: 2 additions & 2 deletions src/strawchemy/dto/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
DTOMissing,
DTOSkip,
DTOUnset,
ExcludeFields,
FieldIterable,
IncludeFields,
Purpose,
PurposeConfig,
Expand Down Expand Up @@ -685,7 +685,7 @@ def decorator(
model: type[ModelT],
purpose: Purpose,
include: IncludeFields | None = None,
exclude: ExcludeFields | None = None,
exclude: FieldIterable | None = None,
partial: bool | None = None,
type_map: Mapping[Any, Any] | None = None,
aliases: Mapping[str, str] | None = None,
Expand Down
82 changes: 76 additions & 6 deletions src/strawchemy/dto/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from enum import Enum
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, final, get_type_hints

from typing_extensions import override
from typing_extensions import Self, override

from strawchemy.utils.annotation import get_annotations

Expand All @@ -23,15 +23,15 @@
"DTOScope",
"DTOSkip",
"DTOUnset",
"ExcludeFields",
"FieldIterable",
"IncludeFields",
"Purpose",
"PurposeConfig",
)

DTOScope: TypeAlias = Literal["global", "dto"]
IncludeFields: TypeAlias = "list[str] | set[str] | Literal['all']"
ExcludeFields: TypeAlias = "list[str] | set[str]"
FieldIterable: TypeAlias = "list[str] | set[str] | frozenset[str] | tuple[str, ...]"
IncludeFields: TypeAlias = "FieldIterable | Literal['all']"


@final
Expand Down Expand Up @@ -160,7 +160,7 @@ class DTOConfig:
"""Configure the DTO for "read" or "write" operations."""
include: IncludeFields = field(default_factory=set)
"""Explicitly include fields from the generated DTO."""
exclude: ExcludeFields = field(default_factory=set)
exclude: FieldIterable = field(default_factory=set)
"""Explicitly exclude fields from the generated DTO. Implies `include="all"`."""
partial: bool | None = None
"""Make all field optional."""
Expand All @@ -185,11 +185,41 @@ def __post_init__(self) -> None:
if self.exclude:
self.include = "all"

@classmethod
def from_include(cls, include: IncludeFields | bool | None = None, purpose: Purpose = Purpose.READ) -> Self:
"""Create a DTOConfig from an include specification.

Factory method for creating a DTOConfig with a simplified interface, converting
an `IncludeFields` specification into a complete configuration object. This is
useful for building configs when only the include/exclude specification matters.

Args:
include: The field inclusion specification. Can be:
- None: Include no fields (converted to empty set)
- "all": Include all fields
- list or set of field names: Include only these specific fields
Defaults to None.
purpose: The purpose of the DTO being configured (READ, WRITE, or COMPLETE).
Defaults to Purpose.READ.

Returns:
A new DTOConfig instance with the specified include and purpose settings.
All other configuration parameters use their defaults.
"""
match include:
case True:
include_ = "all"
case False | None:
include_ = set()
case _:
include_ = include
return cls(purpose, include=include_)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider making include keyword-only to avoid boolean positional argument ambiguity.

The static analysis flags include: IncludeFields | bool | None as a boolean-typed positional argument (FBT001). While functional, callers might accidentally pass a boolean in an unintended position.

🔎 Proposed fix
 @classmethod
-def from_include(cls, include: IncludeFields | bool | None = None, purpose: Purpose = Purpose.READ) -> Self:
+def from_include(cls, *, include: IncludeFields | bool | None = None, purpose: Purpose = Purpose.READ) -> Self:
🧰 Tools
🪛 Ruff (0.14.10)

189-189: Boolean-typed positional argument in function definition

(FBT001)

🤖 Prompt for AI Agents
In src/strawchemy/dto/types.py around lines 188 to 216, the from_include method
accepts include as a positional argument which can be misused by passing a
boolean positionally; change the signature to make include keyword-only (e.g.,
introduce a single '*' before include) so callers must pass include=<...>, keep
purpose as a normal parameter with its default, and update any call sites to use
the new keyword for include; no other logic changes required.


def copy_with(
self,
purpose: Purpose | type[DTOUnset] = DTOUnset,
include: IncludeFields | None = None,
exclude: ExcludeFields | None = None,
exclude: FieldIterable | None = None,
partial: bool | None | type[DTOUnset] = DTOUnset,
unset_sentinel: Any | type[DTOUnset] = DTOUnset,
type_overrides: Mapping[Any, Any] | type[DTOUnset] = DTOUnset,
Expand Down Expand Up @@ -265,3 +295,43 @@ def alias(self, name: str) -> str | None:
if self.alias_generator is not None:
return self.alias_generator(name)
return None

def is_field_included(self, name: str) -> bool:
"""Check if a field should be included based on this configuration.

Evaluates field inclusion using the following rules:
1. If include="all": the field is included unless explicitly excluded
2. If include is a specific list/set: the field is included only if named
3. If include is empty: the field is never included
4. Regardless of include, the field is excluded if it's in the exclude set

This method is used during DTO factory operations to determine which fields
from the source model should be included in the generated DTO.

Args:
name: The field name to check for inclusion.

Returns:
True if the field should be included based on the include/exclude rules,
False otherwise.

Examples:
# include="all" case
config = DTOConfig.from_include("all")
config.is_field_included("any_field") # Returns: True

# Specific fields case
config = DTOConfig.from_include(["field1", "field2"])
config.is_field_included("field1") # Returns: True
config.is_field_included("field3") # Returns: False

# Empty include case
config = DTOConfig.from_include(None)
config.is_field_included("any_field") # Returns: False

# With exclude
config = DTOConfig(Purpose.READ, exclude={"password"}) # include="all"
config.is_field_included("name") # Returns: True
config.is_field_included("password") # Returns: False
"""
return (name in self.include or self.include == "all") and name not in self.exclude
4 changes: 2 additions & 2 deletions src/strawchemy/dto/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
DTOConfig,
DTOFieldConfig,
DTOScope,
ExcludeFields,
FieldIterable,
IncludeFields,
Purpose,
PurposeConfig,
Expand All @@ -45,7 +45,7 @@
def config(
purpose: Purpose,
include: IncludeFields | None = None,
exclude: ExcludeFields | None = None,
exclude: FieldIterable | None = None,
partial: bool | None = None,
type_map: Mapping[Any, Any] | None = None,
aliases: Mapping[str, str] | None = None,
Expand Down
6 changes: 5 additions & 1 deletion src/strawchemy/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,11 @@ def field(
repository_type_ = repository_type if repository_type is not None else self.config.repository_type
execution_options_ = execution_options if execution_options is not None else self.config.execution_options
pagination = (
DefaultOffsetPagination(limit=self.config.pagination_default_limit) if pagination is True else pagination
DefaultOffsetPagination(
limit=self.config.pagination_default_limit, offset=self.config.pagination_default_offset
)
if pagination is True
else pagination
)
if pagination is None:
pagination = self.config.pagination
Expand Down
Loading
Loading