diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 061b9c96..ab358bf6 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -3,6 +3,7 @@ name: 🔂 Tests and linting
env:
COLUMNS: 120 # Makes the error summary table printed by pytest-pretty much easier to read
PROJECT_NAME: "strawchemy"
+ UV_PYTHON_INSTALL_DIR: ${{ github.workspace }}/.uv-python
on:
push:
@@ -49,6 +50,11 @@ jobs:
cache: true
log_level: debug
+ - name: Setup uv
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ enable-cache: true
+
- name: Generate test matrix
id: set-matrix
shell: bash
@@ -74,19 +80,24 @@ jobs:
id: docker-buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- - name: Pip and nox cache
+ - name: nox cache
id: cache
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
with:
path: |
- ~/.cache
~/.nox
.nox
+ ${{ github.workspace }}/.uv-python
key:
- ${{ runner.os }}-nox-${{ matrix.session.session }}-${{ env.pythonLocation }}-${{
+ ${{ runner.os }}-nox-${{ matrix.session.session }}-${{
hashFiles('**/uv.lock') }}-${{ hashFiles('**/noxfile.py') }}
restore-keys: |
- ${{ runner.os }}-nox-${{ matrix.session.session }}-${{ env.pythonLocation }}
+ ${{ runner.os }}-nox-${{ matrix.session.session }}
+
+ - name: Setup uv
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ enable-cache: true
- name: Setup mise
uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3
@@ -196,6 +207,11 @@ jobs:
cache: true
log_level: debug
+ - name: Setup uv
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
+ with:
+ enable-cache: true
+
- name: Install dependencies
run: mise run uv:install
diff --git a/.gitignore b/.gitignore
index 956f00ab..44b16a7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -196,7 +196,6 @@ mise.local.toml
*.sqlite
.windsurf
-
CLAUDE.md
-
.serena
+docs/superpowers
diff --git a/README.md b/README.md
index e1c24392..2bc61e76 100644
--- a/README.md
+++ b/README.md
@@ -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)
@@ -567,9 +568,11 @@ def farms(self) -> str:
Strawchemy supports offset-based pagination out of the box.
-Pagination example:
+Pagination examples
-Enable pagination on fields:
+### Field-Level Pagination
+
+Enable pagination on specific fields:
```python
from strawchemy.schema.pagination import DefaultOffsetPagination
@@ -577,10 +580,13 @@ 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:
@@ -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
```
@@ -619,6 +655,118 @@ Then in your GraphQL queries:
+## Ordering
+
+Strawchemy provides flexible ordering capabilities for query results.
+
+
+Ordering examples
+
+### 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
+ }
+ }
+}
+```
+
+
+
## Filtering
Strawchemy provides powerful filtering capabilities.
@@ -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. |
+| `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
@@ -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",
)
)
diff --git a/examples/testapp/pyproject.toml b/examples/testapp/pyproject.toml
index 1e21d14c..b5d80cb7 100644
--- a/examples/testapp/pyproject.toml
+++ b/examples/testapp/pyproject.toml
@@ -5,11 +5,11 @@ description = "Basic test app"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
- "aiosqlite",
- "litestar[sqlalchemy,standard]",
- "pydantic",
- "sqlalchemy",
- "strawberry-graphql",
+ "aiosqlite",
+ "litestar[sqlalchemy,standard]",
+ "pydantic",
+ "sqlalchemy",
+ "strawberry-graphql",
]
[build-system]
diff --git a/examples/testapp/testapp/types.py b/examples/testapp/testapp/types.py
index 27d9f3d1..abad7872 100644
--- a/examples/testapp/testapp/types.py
+++ b/examples/testapp/testapp/types.py
@@ -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
@@ -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)
class TicketType: ...
@@ -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: ...
@@ -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: ...
diff --git a/mise.toml b/mise.toml
index cc0d28d2..8f5c03a2 100644
--- a/mise.toml
+++ b/mise.toml
@@ -4,15 +4,14 @@ _.python.venv = { path = ".venv" }
_.file = ".env"
[tools]
-uv = "latest"
jq = "latest"
pre-commit = "latest"
actionlint = "latest"
-python = "{{ get_env(name='PYTHON_VERSION', default='3.13') }}"
[vars]
local_pytest_options = "-n=6 -vv"
pytest_coverage_options = "--cov-config=./pyproject.toml --cov=src --cov-report=html"
+ci_uv_run_options = " --frozen --no-default-groups --only-group=nox"
ci_pytest_coverage_options = "--cov-config=./pyproject.toml --cov=src --junit-xml=./junit.xml -o junit_family=legacy"
cleanable_paths = '''{{
[
@@ -43,8 +42,7 @@ cleanable_paths = '''{{
[tasks."uv:install"]
description = "Install dependencies"
-run = "uv sync --all-extras --dev"
-
+run = "uv sync --all-extras"
[tasks.install]
description = "Install dependencies and pre-commit hooks"
@@ -63,80 +61,115 @@ run = "pre-commit install --install-hooks 2>&1"
description = "Run tests"
depends = "uv:install"
alias = "t"
-usage = 'arg "" default=""'
-run = 'uv run pytest {{vars.local_pytest_options}} {{arg(name="test", var=true, default="")}}'
+usage = 'arg "[test]" var=#true'
+run = 'uv run pytest {{vars.local_pytest_options}} ${usage_test-}'
[tasks."test:coverage"]
description = "Run tests with coverage"
depends = "uv:install"
alias = "tc"
-usage = 'arg "" default=""'
-run = 'uv run pytest {{vars.local_pytest_options}} {{vars.pytest_coverage_options}} {{arg(name="test", var=true, default="")}}'
+usage = 'arg "[test]" var=#true'
+run = 'uv run pytest {{vars.local_pytest_options}} {{vars.pytest_coverage_options}} ${usage_test-}'
[tasks."test:unit"]
description = "Run unit tests"
depends = "uv:install"
alias = "tu"
-usage = 'arg "" default=""'
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -t unit -- tests/unit {{vars.local_pytest_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -t unit -- tests/unit {{vars.local_pytest_options}} ${usage_test-}'
[tasks."test:unit:no-extras"]
description = "Run unit tests without extras dependencies"
depends = "uv:install"
alias = "tug"
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -s unit-no-extras -- tests/unit {{vars.local_pytest_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -s unit-no-extras -- tests/unit {{vars.local_pytest_options}} ${usage_test-}'
[tasks."test:unit:coverage"]
description = "Run unit tests with coverage"
depends = "uv:install"
alias = "tuc"
-usage = 'arg "" default=""'
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -t unit -- tests/unit {{vars.local_pytest_options}} {{vars.pytest_coverage_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -t unit -- tests/unit {{vars.local_pytest_options}} {{vars.pytest_coverage_options}} ${usage_test-}'
[tasks."test:integration"]
description = "Run integration tests"
depends = "uv:install"
alias = "ti"
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -s integration -- {{vars.local_pytest_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -s integration -- {{vars.local_pytest_options}} ${usage_test-}'
[tasks."test:integration-postgres"]
description = "Run integration tests"
depends = "uv:install"
alias = "ti-postgres"
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -t postgres -- {{vars.local_pytest_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -t postgres -- {{vars.local_pytest_options}} ${usage_test-}'
[tasks."test:integration-mysql"]
description = "Run integration tests"
depends = "uv:install"
alias = "ti-mysql"
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -t mysql -- {{vars.local_pytest_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -t mysql -- {{vars.local_pytest_options}} ${usage_test-}'
[tasks."test:integration-sqlite"]
description = "Run integration tests"
depends = "uv:install"
alias = "ti-sqlite"
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -t sqlite -- {{vars.local_pytest_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -t sqlite -- {{vars.local_pytest_options}} ${usage_test-}'
[tasks."test:integration:coverage"]
description = "Run integration tests with coverage"
depends = "uv:install"
alias = "tic"
-usage = 'arg "" default=""'
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -s integration -- {{vars.local_pytest_options}} {{vars.pytest_coverage_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -s integration -- {{vars.local_pytest_options}} {{vars.pytest_coverage_options}} ${usage_test-}'
[tasks."test:unit-all"]
description = "Run unit tests on all supported python versions"
depends = "uv:install"
alias = "tua"
-usage = 'arg "" default=""'
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -t unit -- {{vars.local_pytest_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -t unit -- {{vars.local_pytest_options}} ${usage_test-}'
[tasks."test:integration-all"]
description = "Run integration tests on all supported python versions"
depends = "uv:install"
alias = "tia"
-usage = 'arg "" default=""'
-run = 'uv run nox -r -P {{option(name="python", default="3.13")}} -s integration -- {{vars.local_pytest_options}} {{arg(name="test", var=true, default="")}}'
+usage = '''
+flag "--python " default="3.13"
+arg "[test]" var=#true
+'''
+run = 'uv run nox -r -P ${usage_python-} -s integration -- {{vars.local_pytest_options}} ${usage_test-}'
[tasks."test:update-snapshots"]
description = "Run snapshot-based tests and update snapshots"
@@ -149,14 +182,14 @@ run = "uv run pytest {{vars.local_pytest_options}} -m snapshot --snapshot-update
[tasks."ci:install"]
description = "Install dependencies and pre-commit hooks"
-run = "uv venv --allow-existing && uv pip install nox nox-uv"
+run = "uv venv --allow-existing"
[tasks."ci:test"]
description = "Run tests in CI"
usage = 'arg ""'
depends = "ci:install"
env = { UV_PYTHON_PREFERENCE = "only-managed" }
-run = "uv run nox -t tests -s '{{arg(name='session')}}' -- -n=2 {{vars.ci_pytest_coverage_options}}"
+run = "uv run {{vars.ci_uv_run_options}} nox -s \"${usage_session-}\" -- -n=2 {{vars.ci_pytest_coverage_options}}"
[tasks."ci:lint"]
description = "Lint CI yaml files"
@@ -166,7 +199,7 @@ run = "actionlint -shellcheck ''"
description = "Output test matrix for CI"
depends = "ci:install"
run = '''
- uv run nox --json -t tests -t ci -l |
+ uv run {{vars.ci_uv_run_options}} nox --json -t tests -t ci -l |
jq 'map(
{
session,
@@ -181,7 +214,7 @@ run = '''
[tasks."ci:test-sessions"]
description = "Output test session names for CI"
depends = "ci:install"
-run = "uv run nox --json -t tests -l | jq 'map(.name) | unique'"
+run = "uv run {{vars.ci_uv_run_options}} nox --json -t tests -l | jq 'map(.name) | unique'"
# ###############
# Linting
@@ -209,12 +242,18 @@ run = "uv run basedpyright"
[tasks.vulture]
description = "Run vulture"
-run = "uv run --only-group lint vulture"
+run = "uv run vulture"
[tasks.lint]
description = "Lint the code"
alias = "l"
-depends = ["vulture", "pyright", "ruff:check", "ruff:format:check", "slotscheck"]
+depends = [
+ "vulture",
+ "pyright",
+ "ruff:check",
+ "ruff:format:check",
+ "slotscheck"
+]
[tasks."lint:pre-commit"]
description = "Lint the code in pre-commit hook"
diff --git a/noxfile.py b/noxfile.py
index 6b91d139..bbe82edd 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -15,6 +15,7 @@
here = Path(__file__).parent
nox.options.default_venv_backend = "uv"
+nox.options.reuse_venv = "yes"
nox.options.error_on_external_run = True
nox.options.error_on_missing_interpreters = True
@@ -24,7 +25,7 @@
python=SUPPORTED_PYTHON_VERSIONS,
tags=["tests", "unit", "ci"],
uv_groups=["test"],
- uv_all_extras=True,
+ uv_no_groups=["dev"],
uv_sync_locked=False,
)
def unit_tests(session: Session) -> None:
@@ -38,7 +39,7 @@ def unit_tests(session: Session) -> None:
python=SUPPORTED_PYTHON_VERSIONS,
tags=["tests", "unit", "ci"],
uv_groups=["test"],
- uv_all_extras=False,
+ uv_no_groups=["dev"],
uv_sync_locked=False,
)
def unit_tests_no_extras(session: Session) -> None:
@@ -52,7 +53,7 @@ def unit_tests_no_extras(session: Session) -> None:
python=SUPPORTED_PYTHON_VERSIONS,
tags=["tests", "docker", "integration"],
uv_groups=["test"],
- uv_all_extras=True,
+ uv_no_groups=["dev"],
uv_sync_locked=False,
)
def integration_tests(session: Session) -> None:
@@ -65,8 +66,7 @@ def integration_tests(session: Session) -> None:
name="integration-postgres",
python=SUPPORTED_PYTHON_VERSIONS,
tags=["tests", "docker", "integration", "ci", "postgres"],
- uv_groups=["test"],
- uv_all_extras=True,
+ uv_groups=["test", "postgres"],
uv_sync_locked=False,
)
def integration_postgres_tests(session: Session) -> None:
@@ -79,8 +79,7 @@ def integration_postgres_tests(session: Session) -> None:
name="integration-mysql",
python=SUPPORTED_PYTHON_VERSIONS,
tags=["tests", "docker", "integration", "ci", "mysql"],
- uv_groups=["test"],
- uv_all_extras=True,
+ uv_groups=["test", "mysql"],
uv_sync_locked=False,
)
def integration_mysql_tests(session: Session) -> None:
@@ -93,8 +92,7 @@ def integration_mysql_tests(session: Session) -> None:
name="integration-sqlite",
python=SUPPORTED_PYTHON_VERSIONS,
tags=["tests", "docker", "integration", "ci", "sqlite"],
- uv_groups=["test"],
- uv_all_extras=True,
+ uv_groups=["test", "aiosqlite"],
uv_sync_locked=False,
)
def integration_sqlite_tests(session: Session) -> None:
diff --git a/pyproject.toml b/pyproject.toml
index ae9489fd..a173f1fb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -48,41 +48,38 @@ geo = ["GeoAlchemy2", "geojson-pydantic", "shapely"]
pydantic = ["pydantic"]
[dependency-groups]
-aiosqlite = ["aiosqlite"]
-build = ["bump-my-version", "git-cliff", "unasyncd"]
dev = [
+ "debugpy",
+ "testapp",
+ { include-group = "aiosqlite" },
{ include-group = "build" },
- { include-group = "test" },
- { include-group = "lint" },
{ include-group = "doc" },
- { include-group = "postgres" },
+ { include-group = "lint" },
{ include-group = "mysql" },
- { include-group = "aiosqlite" },
- "testapp",
- "debugpy",
+ { include-group = "nox" },
+ { include-group = "postgres" },
+ { include-group = "test" },
]
+aiosqlite = ["aiosqlite"]
+build = ["bump-my-version", "git-cliff", "unasyncd"]
codeflash = ["codeflash"]
doc = ["git-cliff>=2.6.1"]
-lint = ["basedpyright", "ruff", "vulture", "slotscheck>=0.16.5"]
+lint = ["basedpyright", "ruff", "slotscheck>=0.16.5", "vulture"]
mysql = ["asyncmy", "cryptography"]
+nox = ["nox-uv", "nox[uv]"]
postgres = ["asyncpg>=0.29.0", "psycopg[binary,pool]>=3.2.3"]
test = [
- { include-group = "postgres" },
- { include-group = "mysql" },
- { include-group = "aiosqlite" },
- "testapp",
- "nox[uv]",
- "nox-uv",
+ "covdefaults",
"pytest",
- "pytest-cov",
"pytest-asyncio>=0.24",
- "pytest-pretty",
- "pytest-xdist",
+ "pytest-cov",
"pytest-databases[postgres,mysql]",
"pytest-lazy-fixtures",
- "syrupy",
+ "pytest-pretty",
+ "pytest-xdist",
"sqlparse",
- "covdefaults",
+ "syrupy",
+ "testapp",
]
[build-system]
@@ -100,6 +97,7 @@ exclude = [
"**/.venv",
"**/.tox",
"**/.nox",
+ "**/.uv-python",
"**/build",
"**/dist",
"**/node_modules",
@@ -153,6 +151,14 @@ name = "strawchemy"
version = "{current_version}"
"""
+[tool.codeflash]
+# All paths are relative to this pyproject.toml's directory.
+module-root = "src"
+tests-root = "tests"
+test-framework = "pytest"
+ignore-paths = []
+formatter-cmds = ["ruff check --exit-zero --fix $file", "ruff format $file"]
+
[tool.codespell]
skip = "*.po,*.ts,./src/3rdParty,./src/Test"
ignore-words-list = "nin"
@@ -419,7 +425,7 @@ SQLAlchemyGraphQLAsyncRepository = "SQLAlchemyGraphQLSyncRepository"
StrawchemyAsyncRepository = "StrawchemySyncRepository"
[tool.uv]
-default-groups = ["test", "lint"]
+default-groups = ["dev"]
# unasyncd prevent codeflash to be installed to the latest version, need
conflicts = [[{ group = "build" }, { group = "codeflash" }]]
@@ -437,11 +443,3 @@ paths = ["src", "tests"]
sort_by_size = true
exclude = ["tests/fixtures.py", "tests/unit/schemas/mutations"]
ignore_names = ["target"]
-
-[tool.codeflash]
-# All paths are relative to this pyproject.toml's directory.
-module-root = "src"
-tests-root = "tests"
-test-framework = "pytest"
-ignore-paths = []
-formatter-cmds = ["ruff check --exit-zero --fix $file", "ruff format $file"]
diff --git a/src/strawchemy/config/base.py b/src/strawchemy/config/base.py
index f8e7b9cd..69a0aeba 100644
--- a/src/strawchemy/config/base.py
+++ b/src/strawchemy/config/base.py
@@ -5,7 +5,9 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
+from strawchemy.dto import Purpose
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
@@ -43,17 +45,43 @@ 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 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`."""
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)
+
+ @property
+ def field_config(self) -> DTOConfig:
+ return DTOConfig(purpose=Purpose.READ, global_include=self.include, global_exclude=self.exclude or set())
+
+ @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)
diff --git a/src/strawchemy/dto/base.py b/src/strawchemy/dto/base.py
index b78f55bb..80fee94e 100644
--- a/src/strawchemy/dto/base.py
+++ b/src/strawchemy/dto/base.py
@@ -14,6 +14,7 @@
ClassVar,
ForwardRef,
Generic,
+ Literal,
Optional,
Protocol,
TypeAlias,
@@ -32,7 +33,7 @@
DTOMissing,
DTOSkip,
DTOUnset,
- ExcludeFields,
+ FieldIterable,
IncludeFields,
Purpose,
PurposeConfig,
@@ -392,17 +393,26 @@ def should_exclude_field(
explictly_excluded = node.is_root and field.model_field_name in dto_config.exclude
explicitly_included = node.is_root and field.model_field_name in dto_config.include
- # Exclude fields not present in init if purpose is write
- if dto_config.purpose is Purpose.WRITE and not explicitly_included:
- explictly_excluded = explictly_excluded or not field.init
+ globally_excluded = field.model_field_name in dto_config.global_exclude
+ globally_included = field.model_field_name in dto_config.global_include
+
if dto_config.include == "all" and not explictly_excluded:
- explicitly_included = True
+ explicitly_included = globally_included = True
+
+ if dto_config.global_include == "all" and not globally_excluded:
+ globally_included = True
excluded = dto_config.purpose not in field.allowed_purposes
+
+ # Exclude fields not present in init if purpose is write
+ if dto_config.purpose is Purpose.WRITE and not (explicitly_included or globally_included):
+ excluded = excluded or not field.init
+
if node.is_root:
excluded = excluded or (explictly_excluded or not explicitly_included)
else:
- excluded = excluded or explictly_excluded
+ excluded = excluded or (globally_excluded or not globally_included)
+
return not has_override and excluded
def _resolve_basic_type(self, field: DTOFieldDefinition[ModelT, ModelFieldT], dto_config: DTOConfig) -> Any:
@@ -524,7 +534,7 @@ def _factory(
node: Node[Relation[Any, DTOBaseT], None],
base: type[Any] | None = None,
parent_field_def: DTOFieldDefinition[ModelT, ModelFieldT] | None = None,
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
backend_kwargs: dict[str, Any] | None = None,
**kwargs: Any,
) -> type[DTOBaseT]:
@@ -538,7 +548,7 @@ def _gen() -> Iterable[DTOFieldDefinition[ModelT, ModelFieldT]]:
dto_config=dto_config,
base=base,
node=node,
- raise_if_no_fields=raise_if_no_fields,
+ if_no_fields=if_no_fields,
**kwargs,
)
for field_def in iterable:
@@ -579,7 +589,7 @@ def iter_field_definitions(
dto_config: DTOConfig,
base: type[DTOBase[ModelT]] | None,
node: Node[Relation[ModelT, DTOBaseT], None],
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
**factory_kwargs: Any,
) -> Generator[DTOFieldDefinition[ModelT, ModelFieldT]]:
no_fields = True
@@ -611,7 +621,7 @@ def iter_field_definitions(
if no_fields:
msg = f"{name} DTO generated from {model.__qualname__} have no fields"
- if raise_if_no_fields:
+ if if_no_fields == "raise":
raise EmptyDTOError(msg)
warnings.warn(msg, stacklevel=2)
@@ -621,11 +631,13 @@ def factory(
dto_config: DTOConfig,
base: type[Any] | None = None,
name: str | None = None,
+ *,
parent_field_def: DTOFieldDefinition[ModelT, ModelFieldT] | None = None,
current_node: Node[Relation[Any, DTOBaseT], None] | None = None,
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
tags: set[str] | None = None,
backend_kwargs: dict[str, Any] | None = None,
+ no_cache: bool = False,
**kwargs: Any,
) -> type[DTOBaseT]:
"""Build a Data transfer object (DTO) from an SQAlchemy model."""
@@ -640,7 +652,7 @@ def factory(
if dto_config.scope == "global":
self._scoped_dto_names[self._scoped_cache_key(model, dto_config)] = name
- if (dto := self._dto_cache.get(cache_key)) or (dto := self._dto_cache.get(scoped_cache_key)):
+ if not no_cache and ((dto := self._dto_cache.get(cache_key)) or (dto := self._dto_cache.get(scoped_cache_key))):
return self.backend.copy(dto, name) if node.is_root else dto
dto = self._factory(
@@ -650,7 +662,7 @@ def factory(
node,
base,
parent_field_def,
- raise_if_no_fields,
+ if_no_fields,
backend_kwargs,
**kwargs,
)
@@ -684,8 +696,9 @@ def decorator(
self,
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,
diff --git a/src/strawchemy/dto/strawberry.py b/src/strawchemy/dto/strawberry.py
index a4c545b5..edf91673 100644
--- a/src/strawchemy/dto/strawberry.py
+++ b/src/strawchemy/dto/strawberry.py
@@ -27,6 +27,7 @@
from __future__ import annotations
import dataclasses
+from copy import copy
from dataclasses import dataclass
from enum import Enum
from functools import cached_property
@@ -55,7 +56,7 @@
from strawchemy.utils.text import camel_to_snake
if TYPE_CHECKING:
- from collections.abc import Callable, Hashable, Sequence
+ from collections.abc import Callable, Hashable, Iterable, Sequence
from strawchemy.schema.filters import EqualityComparison, GraphQLComparison
@@ -101,14 +102,60 @@ def is_transform(self) -> bool:
return bool(self.json_path)
-class StrawchemyDTOAttributes:
- __strawchemy_description__: ClassVar[str] = "GraphQL type"
- __strawchemy_is_root_aggregation_type__: ClassVar[bool] = False
- __strawchemy_field_map__: ClassVar[dict[DTOKey, GraphQLFieldDefinition]] = {}
- __strawchemy_query_hook__: ClassVar[QueryHook[Any] | list[QueryHook[Any]] | None] = None
- __strawchemy_filter__: ClassVar[type[Any] | None] = None
- __strawchemy_order_by__: ClassVar[type[Any] | None] = None
- __strawchemy_purpose__: ClassVar[GraphQLPurpose | None] = None
+@dataclass
+class StrawchemyDefinition:
+ description: str = "GraphQL type"
+ is_root_aggregation_type: bool = False
+ field_map: dict[DTOKey, GraphQLFieldDefinition] = dataclasses.field(default_factory=dict)
+ query_hook: QueryHook[Any] | list[QueryHook[Any]] | None = None
+ filter: type[Any] | None = None
+ order_by: type[Any] | None = None
+ distinct_on: type[Any] | None = None
+ purpose: GraphQLPurpose | None = None
+
+ def __copy__(self) -> StrawchemyDefinition:
+ return dataclasses.replace(self, field_map=dict(self.field_map))
+
+ def populate_fields(
+ self,
+ key_source: type[Any] | DTOKey,
+ fields: Iterable[GraphQLFieldDefinition],
+ ) -> Self:
+ key = key_source if isinstance(key_source, DTOKey) else DTOKey([key_source])
+ self.field_map = {key + f.name: f for f in fields}
+ return self
+
+ def get_field(self, key: DTOKey, name: str | None = None) -> GraphQLFieldDefinition:
+ full_key = key + name if name else key
+ return self.field_map[full_key]
+
+ def get_field_or_none(self, key: DTOKey, name: str | None = None) -> GraphQLFieldDefinition | None:
+ full_key = key + name if name else key
+ return self.field_map.get(full_key)
+
+ @property
+ def query_hooks(self) -> list[QueryHook[Any]]:
+ if self.query_hook is None:
+ return []
+ if isinstance(self.query_hook, list):
+ return self.query_hook
+ return [self.query_hook]
+
+ @property
+ def is_update_purpose(self) -> bool:
+ return self.purpose in ("update_by_pk_input", "update_by_filter_input")
+
+
+class StrawchemyObject:
+ __strawchemy_definition__: ClassVar[StrawchemyDefinition]
+
+ def __init_subclass__(cls, **kwargs: Any) -> None:
+ super().__init_subclass__(**kwargs)
+ existing = cls.__dict__.get("__strawchemy_definition__")
+ if existing is None:
+ cls.__strawchemy_definition__ = StrawchemyDefinition()
+ else:
+ cls.__strawchemy_definition__ = copy(existing)
class _Key(Generic[T]):
@@ -129,7 +176,9 @@ class _Key(Generic[T]):
string.
"""
- separator: str = ":"
+ __slots__ = ("_key",)
+
+ separator: ClassVar[str] = ":"
def __init__(self, components: Sequence[T | str] | str | None = None) -> None:
self._key: str = ""
@@ -425,10 +474,10 @@ class EnumDTO(DTOBase[Any], Enum):
def field_definition(self) -> GraphQLFieldDefinition: ...
-class MappedStrawberryGraphQLDTO(StrawchemyDTOAttributes, MappedStrawberryDTO[ModelT]): ...
+class MappedStrawberryGraphQLDTO(StrawchemyObject, MappedStrawberryDTO[ModelT]): ...
-class UnmappedStrawberryGraphQLDTO(StrawchemyDTOAttributes, StrawberryDTO[ModelT]): ...
+class UnmappedStrawberryGraphQLDTO(StrawchemyObject, StrawberryDTO[ModelT]): ...
class GraphQLFilterDTO(UnmappedStrawberryGraphQLDTO[DeclarativeBase]):
@@ -455,7 +504,7 @@ def tree(self, _node: QueryNodeType | None = None) -> QueryNodeType:
for name in self.dto_set_fields:
value: OrderByDTO | OrderByEnum = getattr(self, name)
- field = self.__strawchemy_field_map__[key + name]
+ field = self.__strawchemy_definition__.get_field(key, name)
if isinstance(field, FunctionFieldDefinition) and not field.has_model_field:
field.model_field = node.value.model_field
if isinstance(value, OrderByDTO):
@@ -482,7 +531,7 @@ def filters_tree(self, _node: QueryNodeType | None = None) -> tuple[QueryNodeTyp
)
for name in self.dto_set_fields:
value: EqualityComparison[Any] | BooleanFilterDTO | AggregateFilterDTO = getattr(self, name)
- field = self.__strawchemy_field_map__[key + name]
+ field = self.__strawchemy_definition__.get_field(key, name)
if isinstance(value, BooleanFilterDTO):
child, _ = node.upsert_child(field, match_on="value_equality")
_, sub_query = value.filters_tree(child)
diff --git a/src/strawchemy/dto/types.py b/src/strawchemy/dto/types.py
index 9722d3c6..491bf10d 100644
--- a/src/strawchemy/dto/types.py
+++ b/src/strawchemy/dto/types.py
@@ -5,9 +5,9 @@
import dataclasses
from dataclasses import dataclass, field
from enum import Enum
-from typing import TYPE_CHECKING, Any, Literal, TypeAlias, final, get_type_hints
+from typing import TYPE_CHECKING, Any, Literal, TypeAlias, final, get_type_hints, overload
-from typing_extensions import override
+from typing_extensions import Self, TypeIs, override
from strawchemy.utils.annotation import get_annotations
@@ -23,15 +23,17 @@
"DTOScope",
"DTOSkip",
"DTOUnset",
- "ExcludeFields",
+ "FieldIterable",
"IncludeFields",
"Purpose",
"PurposeConfig",
+ "cast_include_fields",
+ "is_fields_iterable",
)
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
@@ -160,8 +162,12 @@ 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)
+ global_include: IncludeFields = field(default_factory=set)
+ """Explicitly include fields from the generated DTO and all its children."""
+ exclude: FieldIterable = field(default_factory=set)
"""Explicitly exclude fields from the generated DTO. Implies `include="all"`."""
+ global_exclude: FieldIterable = field(default_factory=set)
+ """Explicitly exclude fields from the generated DTO and all its children. Implies `global_include="all"`."""
partial: bool | None = None
"""Make all field optional."""
partial_default: Any = None
@@ -182,14 +188,79 @@ def __post_init__(self) -> None:
if self.include and self.include != "all" and self.exclude:
msg = "When using `exclude` you must set `include='all' or leave it unset`"
raise ValueError(msg)
+ if self.global_include and self.global_include != "all" and self.global_exclude:
+ msg = "When using `global_exclude` you must set `global_include='all' or leave it unset`"
+ raise ValueError(msg)
+ if self.global_exclude:
+ self.global_include = "all"
if self.exclude:
self.include = "all"
+ @overload
+ @classmethod
+ def _merge_field_iterables(cls, *iterables: FieldIterable) -> FieldIterable: ...
+
+ @overload
+ @classmethod
+ def _merge_field_iterables(cls, *iterables: IncludeFields) -> IncludeFields: ...
+
+ @classmethod
+ def _merge_field_iterables(cls, *iterables: IncludeFields | FieldIterable) -> IncludeFields | FieldIterable:
+ if any(iterable == "all" for iterable in iterables):
+ return "all"
+ return set().union(*iterables)
+
+ def union(self, other: DTOConfig) -> DTOConfig:
+ include = self._merge_field_iterables(self.include, other.include)
+ exclude = self._merge_field_iterables(self.exclude, other.exclude)
+ global_include = self._merge_field_iterables(self.global_include, other.global_include)
+ global_exclude = self._merge_field_iterables(self.global_exclude, other.global_exclude)
+ type_overrides = dict(self.type_overrides) | dict(other.type_overrides)
+ annotation_overrides = self.annotation_overrides | other.annotation_overrides
+ tags = self.tags | other.tags
+
+ return self.copy_with(
+ include=include,
+ global_include=global_include,
+ exclude=exclude,
+ global_exclude=global_exclude,
+ type_overrides=type_overrides,
+ annotation_overrides=annotation_overrides,
+ tags=tags,
+ )
+
+ @classmethod
+ def from_include(
+ cls, include: IncludeFields | Literal[False] | 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.
+ """
+ return cls(purpose, include=include if include else set())
+
def copy_with(
self,
purpose: Purpose | type[DTOUnset] = DTOUnset,
include: IncludeFields | None = None,
- exclude: ExcludeFields | None = None,
+ global_include: IncludeFields | None = None,
+ exclude: FieldIterable | None = None,
+ global_exclude: FieldIterable | None = None,
partial: bool | None | type[DTOUnset] = DTOUnset,
unset_sentinel: Any | type[DTOUnset] = DTOUnset,
type_overrides: Mapping[Any, Any] | type[DTOUnset] = DTOUnset,
@@ -206,11 +277,18 @@ def copy_with(
if include is None and exclude is None:
include, exclude = self.include, self.exclude
else:
- include = include or []
- exclude = exclude or []
+ include = include or set()
+ exclude = exclude or set()
+ if global_include is None and global_exclude is None:
+ global_include, global_exclude = self.global_include, self.global_exclude
+ else:
+ global_include = global_include or set()
+ global_exclude = global_exclude or set()
return DTOConfig(
include=include,
exclude=exclude,
+ global_include=global_include,
+ global_exclude=global_exclude,
purpose=self.purpose if purpose is DTOUnset else purpose,
partial=self.partial if partial is DTOUnset else partial,
unset_sentinel=self.unset_sentinel if unset_sentinel is DTOUnset else unset_sentinel,
@@ -265,3 +343,55 @@ 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.
+
+ 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.
+ """
+ if self.include == "all":
+ return name not in self.exclude
+ if self.global_include == "all":
+ return name not in self.global_exclude
+
+ included = set(self.include) | set(self.global_include)
+ excluded = set(self.exclude) | set(self.global_exclude)
+ return name in included and name not in excluded
+
+ def __or__(self, other: DTOConfig) -> DTOConfig:
+ return self.union(other)
+
+
+@overload
+def cast_include_fields(value: Literal["all"]) -> Literal["all"]: ...
+
+
+@overload
+def cast_include_fields(value: frozenset[str] | set[str] | list[str] | tuple[str, ...] | None) -> frozenset[str]: ...
+
+
+def cast_include_fields(value: IncludeFields | None) -> frozenset[str] | Literal["all"]:
+ match value:
+ case None:
+ return frozenset()
+ case "all":
+ return "all"
+ case _:
+ return frozenset(value)
+
+
+def is_fields_iterable(value: Any) -> TypeIs[IncludeFields | FieldIterable | None]:
+ """Test the given value is suitable to be used as either `include` or `exclude` in a DTOConfig."""
+ if value == "all" or value is None:
+ return True
+ if isinstance(value, str):
+ return False
+ return isinstance(value, (frozenset, set, list, tuple))
diff --git a/src/strawchemy/dto/utils.py b/src/strawchemy/dto/utils.py
index cd53c789..af815036 100644
--- a/src/strawchemy/dto/utils.py
+++ b/src/strawchemy/dto/utils.py
@@ -19,7 +19,7 @@
DTOConfig,
DTOFieldConfig,
DTOScope,
- ExcludeFields,
+ FieldIterable,
IncludeFields,
Purpose,
PurposeConfig,
@@ -45,7 +45,9 @@
def config(
purpose: Purpose,
include: IncludeFields | None = None,
- exclude: ExcludeFields | None = None,
+ exclude: FieldIterable | None = None,
+ global_include: IncludeFields | None = None,
+ global_exclude: FieldIterable | None = None,
partial: bool | None = None,
type_map: Mapping[Any, Any] | None = None,
aliases: Mapping[str, str] | None = None,
@@ -58,6 +60,10 @@ def config(
config.exclude = exclude
if include:
config.include = include
+ if global_include:
+ config.global_include = global_include
+ if global_exclude:
+ config.global_exclude = global_exclude
if type_map:
config.type_overrides = type_map
if aliases:
diff --git a/src/strawchemy/mapper.py b/src/strawchemy/mapper.py
index 5769cded..1b7405a0 100644
--- a/src/strawchemy/mapper.py
+++ b/src/strawchemy/mapper.py
@@ -2,7 +2,7 @@
import dataclasses
from functools import cached_property, partial
-from typing import TYPE_CHECKING, Any, TypeVar, overload
+from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload
from strawberry.annotation import StrawberryAnnotation
from strawberry.schema.config import StrawberryConfig
@@ -11,21 +11,22 @@
from strawchemy.dto.backend.strawberry import StrawberrryDTOBackend
from strawchemy.dto.base import TYPING_NS
from strawchemy.dto.strawberry import BooleanFilterDTO, EnumDTO, MappedStrawberryGraphQLDTO, OrderByDTO, OrderByEnum
+from strawchemy.dto.utils import read_all_config
from strawchemy.schema.factories import (
- AggregateFilterDTOFactory,
- BooleanFilterDTOFactory,
- DistinctOnFieldsDTOFactory,
- EnumDTOBackend,
- EnumDTOFactory,
- InputFactory,
- OrderByDTOFactory,
- RootAggregateTypeDTOFactory,
- TypeDTOFactory,
- UpsertConflictFieldsDTOFactory,
- UpsertConflictFieldsEnumDTOBackend,
+ AggregateFilterFactory,
+ AggregateRootTypeFactory,
+ BooleanFilterFactory,
+ DistinctOnEnumFactory,
+ EnumBackend,
+ EnumFactory,
+ MutationInputFactory,
+ ObjectTypeFactory,
+ OrderByFactory,
+ UpsertConflictEnumBackend,
+ UpsertConflictEnumFactory,
)
from strawchemy.schema.field import StrawchemyField
-from strawchemy.schema.mutation import types
+from strawchemy.schema.mutation import types as mutation_types
from strawchemy.schema.mutation.field_builder import MutationFieldBuilder
from strawchemy.schema.mutation.fields import (
StrawchemyCreateMutationField,
@@ -33,7 +34,6 @@
StrawchemyUpdateMutationField,
StrawchemyUpsertMutationField,
)
-from strawchemy.schema.pagination import DefaultOffsetPagination
from strawchemy.utils.registry import StrawberryRegistry
if TYPE_CHECKING:
@@ -44,7 +44,9 @@
from strawberry.extensions.field_extension import FieldExtension
from strawberry.types.arguments import StrawberryArgument
+ from strawchemy.dto.types import IncludeFields
from strawchemy.repository.typing import QueryHookCallable
+ from strawchemy.schema.pagination import DefaultOffsetPagination
from strawchemy.transpiler.hook import QueryHook
from strawchemy.typing import AnyRepositoryType, FilterStatementCallable, MappedGraphQLDTO, SupportedDialect
from strawchemy.validation.base import ValidationProtocol
@@ -53,7 +55,6 @@
T = TypeVar("T", bound="DeclarativeBase")
-_TYPES_NS = TYPING_NS | vars(types)
__all__ = ("Strawchemy",)
@@ -83,6 +84,8 @@ class Strawchemy:
pydantic (PydanticMapper): A mapper for generating Pydantic models.
"""
+ _types_namespace: ClassVar[dict[str, Any]] = TYPING_NS | vars(mutation_types)
+
def __init__(
self,
config: StrawchemyConfig | SupportedDialect,
@@ -103,41 +106,47 @@ def __init__(
self.registry = StrawberryRegistry(strawberry_config or StrawberryConfig())
strawberry_backend = StrawberrryDTOBackend(MappedStrawberryGraphQLDTO)
- enum_backend = EnumDTOBackend(self.config.auto_snake_case)
- upsert_conflict_fields_enum_backend = UpsertConflictFieldsEnumDTOBackend(
+ enum_backend = EnumBackend(self.config.auto_snake_case)
+ upsert_conflict_fields_enum_backend = UpsertConflictEnumBackend(
self.config.inspector, self.config.auto_snake_case
)
- self._aggregate_filter_factory = AggregateFilterDTOFactory(self)
- self._order_by_factory = OrderByDTOFactory(self)
- self._distinct_on_enum_factory = DistinctOnFieldsDTOFactory(self.config.inspector)
- self._type_factory = TypeDTOFactory(self, strawberry_backend, order_by_factory=self._order_by_factory)
- self._input_factory = InputFactory(self, strawberry_backend)
- self._aggregation_factory = RootAggregateTypeDTOFactory(
- self, strawberry_backend, type_factory=self._type_factory
+ self.aggregate_filter_factory = AggregateFilterFactory(self)
+ self.order_by_factory = OrderByFactory(self)
+ self.distinct_on_enum_factory = DistinctOnEnumFactory(self)
+ self.type_factory = ObjectTypeFactory(
+ self,
+ strawberry_backend,
+ order_by_factory=self.order_by_factory,
+ distinct_on_factory=self.distinct_on_enum_factory,
)
- self._enum_factory = EnumDTOFactory(self.config.inspector, enum_backend)
- self._filter_factory = BooleanFilterDTOFactory(self, aggregate_filter_factory=self._aggregate_filter_factory)
- self._upsert_conflict_factory = UpsertConflictFieldsDTOFactory(
- self.config.inspector, upsert_conflict_fields_enum_backend
+ self.input_factory = MutationInputFactory(self, strawberry_backend)
+ self.aggregation_factory = AggregateRootTypeFactory(self, strawberry_backend, type_factory=self.type_factory)
+ self.enum_factory = EnumFactory(self, enum_backend)
+ self.filter_factory = BooleanFilterFactory(self, aggregate_filter_factory=self.aggregate_filter_factory)
+ self.upsert_conflict_factory = UpsertConflictEnumFactory(self, upsert_conflict_fields_enum_backend)
+
+ self.filter = self.filter_factory.input
+ self.aggregate_filter = partial(self.aggregate_filter_factory.input, mode="aggregate_filter")
+ self.distinct_on = self.distinct_on_enum_factory.decorator
+ self.input = self.input_factory.input
+ self.create_input = partial(self.input_factory.input, mode="create_input")
+ self.pk_update_input = partial(self.input_factory.input, mode="update_by_pk_input")
+ self.filter_update_input = partial(self.input_factory.input, mode="update_by_filter_input")
+ self.order = partial(self.order_by_factory.input, mode="order_by")
+ self.type = self.type_factory.type
+ self.aggregate = partial(self.aggregation_factory.type, mode="aggregate_type")
+ self.upsert_update_fields = self.enum_factory.input
+ self.upsert_conflict_fields = self.upsert_conflict_factory.input
+ self._mutation_builder = MutationFieldBuilder(
+ config=self.config,
+ registry_namespace_getter=self._annotation_namespace,
+ order_by_factory=self.order_by_factory,
+ filter_factory=self.filter_factory,
+ distinct_on_factory=self.distinct_on_enum_factory,
)
-
- self.filter = self._filter_factory.input
- self.aggregate_filter = partial(self._aggregate_filter_factory.input, mode="aggregate_filter")
- self.distinct_on = self._distinct_on_enum_factory.decorator
- self.input = self._input_factory.input
- self.create_input = partial(self._input_factory.input, mode="create_input")
- self.pk_update_input = partial(self._input_factory.input, mode="update_by_pk_input")
- self.filter_update_input = partial(self._input_factory.input, mode="update_by_filter_input")
- self.order = partial(self._order_by_factory.input, mode="order_by")
- self.type = self._type_factory.type
- self.aggregate = partial(self._aggregation_factory.type, mode="aggregate_type")
- self.upsert_update_fields = self._enum_factory.input
- self.upsert_conflict_fields = self._upsert_conflict_factory.input
- # Initialize mutation field builder
- self._mutation_builder = MutationFieldBuilder(self.config, self._annotation_namespace)
# Register common types
- self.registry.register_enum(OrderByEnum, "OrderByEnum")
+ self.registry.register_enum(OrderByEnum, dto_config=read_all_config)
def _annotation_namespace(self) -> dict[str, Any]:
"""Provides the namespace for Strawberry annotations.
@@ -147,7 +156,7 @@ def _annotation_namespace(self) -> dict[str, Any]:
Returns:
A dictionary representing the annotation namespace.
"""
- return self.registry.namespace("object") | _TYPES_NS
+ return self.registry.namespace("object") | self._types_namespace
@cached_property
def pydantic(self) -> PydanticMapper:
@@ -168,10 +177,10 @@ def field(
self,
resolver: Any,
*,
- filter_input: type[BooleanFilterDTO] | None = None,
- order_by: type[OrderByDTO] | None = None,
- distinct_on: type[EnumDTO] | None = None,
+ filter_input: type[BooleanFilterDTO] | bool | None = None,
+ order_by: IncludeFields | type[OrderByDTO] | None = None,
pagination: bool | DefaultOffsetPagination | None = None,
+ distinct_on: IncludeFields | type[EnumDTO] | None = None,
arguments: list[StrawberryArgument] | None = None,
id_field_name: str | None = None,
root_aggregations: bool = False,
@@ -196,10 +205,10 @@ def field(
def field(
self,
*,
- filter_input: type[BooleanFilterDTO] | None = None,
- order_by: type[OrderByDTO] | None = None,
- distinct_on: type[EnumDTO] | None = None,
+ filter_input: type[BooleanFilterDTO] | bool | None = None,
+ order_by: IncludeFields | type[OrderByDTO] | None = None,
pagination: bool | DefaultOffsetPagination | None = None,
+ distinct_on: IncludeFields | type[EnumDTO] | None = None,
arguments: list[StrawberryArgument] | None = None,
id_field_name: str | None = None,
root_aggregations: bool = False,
@@ -224,10 +233,10 @@ def field(
self,
resolver: Any | None = None,
*,
- filter_input: type[BooleanFilterDTO] | None = None,
- order_by: type[OrderByDTO] | None = None,
- distinct_on: type[EnumDTO] | None = None,
+ filter_input: type[BooleanFilterDTO] | bool | None = None,
+ order_by: IncludeFields | type[OrderByDTO] | None = None,
pagination: bool | DefaultOffsetPagination | None = None,
+ distinct_on: IncludeFields | type[EnumDTO] | None = None,
arguments: list[StrawberryArgument] | None = None,
id_field_name: str | None = None,
root_aggregations: bool = False,
@@ -285,21 +294,13 @@ def field(
"""
namespace = self._annotation_namespace()
type_annotation = StrawberryAnnotation.from_annotation(graphql_type, namespace) if graphql_type else None
- 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
- )
- if pagination is None:
- pagination = self.config.pagination
- id_field_name = id_field_name or self.config.default_id_field_name
field = StrawchemyField(
config=self.config,
- repository_type=repository_type_,
+ repository_type=repository_type,
root_field=root_field,
filter_statement=filter_statement,
- execution_options=execution_options_,
+ execution_options=execution_options,
filter_type=filter_input,
order_by=order_by,
pagination=pagination,
@@ -321,6 +322,9 @@ def field(
registry_namespace=namespace,
description=description,
arguments=arguments,
+ order_by_factory=self.order_by_factory,
+ filter_factory=self.filter_factory,
+ distinct_on_factory=self.distinct_on_enum_factory,
)
return field(resolver) if resolver else field
diff --git a/src/strawchemy/repository/strawberry/base.py b/src/strawchemy/repository/strawberry/base.py
index 98b016fb..6c2ae1ae 100644
--- a/src/strawchemy/repository/strawberry/base.py
+++ b/src/strawchemy/repository/strawberry/base.py
@@ -8,7 +8,6 @@
import dataclasses
from collections import defaultdict
-from collections.abc import Collection, Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeVar, overload
@@ -25,21 +24,24 @@
QueryNode,
QueryNodeMetadata,
RelationFilterDTO,
- StrawchemyDTOAttributes,
+ StrawchemyObject,
)
from strawchemy.exceptions import StrawchemyError
from strawchemy.repository.strawberry._node import StrawberryQueryNode
from strawchemy.schema.mutation import error_type_names
+from strawchemy.transpiler import QueryHook
from strawchemy.utils.graph import NodeMetadata
from strawchemy.utils.strawberry import dto_model_from_type, strawberry_contained_user_type
from strawchemy.utils.text import camel_to_snake, snake_keys
if TYPE_CHECKING:
+ from collections.abc import Sequence
+
from strawberry import Info
from strawberry.types.field import StrawberryField
- from strawchemy.transpiler import QueryHook, QueryResult
- from strawchemy.typing import QueryNodeType, StrawchemyTypeWithStrawberryObjectDefinition
+ from strawchemy.transpiler import QueryResult
+ from strawchemy.typing import QueryNodeType, StrawchemyObjectWithStrawberryObjectDefinition
__all__ = ("IS_ASYNC_REPOSITORY", "IS_SYNC_REPOSITORY", "GraphQLResult", "StrawchemyRepository")
@@ -198,15 +200,14 @@ def _get_field_hooks(cls, field: StrawberryField) -> QueryHook[Any] | Sequence[Q
return field.query_hook if isinstance(field, StrawchemyField) else None
- def _add_query_hooks(self, query_hooks: QueryHook[Any] | Sequence[QueryHook[Any]], node: QueryNodeType) -> None:
- hooks = query_hooks if isinstance(query_hooks, Collection) else [query_hooks]
- for hook in hooks:
+ def _add_query_hooks(self, query_hooks: Sequence[QueryHook[Any]], node: QueryNodeType) -> None:
+ for hook in query_hooks:
hook.info_var.set(self.info)
self._query_hooks[node].append(hook)
def _build(
self,
- strawberry_type: type[StrawchemyTypeWithStrawberryObjectDefinition],
+ strawberry_type: type[StrawchemyObjectWithStrawberryObjectDefinition],
selected_fields: list[Selection],
node: QueryNodeType,
) -> None:
@@ -215,8 +216,7 @@ def _build(
selection_type = selection_type.resolve_type()
strawberry_definition = get_object_definition(selection_type, strict=True)
- if selection_type.__strawchemy_query_hook__:
- self._add_query_hooks(selection_type.__strawchemy_query_hook__, node)
+ self._add_query_hooks(selection_type.__strawchemy_definition__.query_hooks, node)
for selection in selected_fields:
if (
@@ -234,20 +234,19 @@ def _build(
dto_model = dto_model_from_type(selection_type)
if (hooks := self._get_field_hooks(strawberry_field)) is not None:
- self._add_query_hooks(hooks, node)
+ self._add_query_hooks([hooks] if isinstance(hooks, QueryHook) else hooks, node)
if has_object_definition(selection_type):
dto = selection_type
else:
msg = f"Unsupported type: {selection_type}"
raise StrawchemyError(msg)
- assert issubclass(dto, StrawchemyDTOAttributes)
+ assert issubclass(dto, StrawchemyObject)
key = DTOKey.from_query_node(QueryNode.root_node(dto_model)) + strawberry_field.name
- try:
- field_definition = dto.__strawchemy_field_map__[key]
- except KeyError:
+ field_definition = dto.__strawchemy_definition__.get_field_or_none(key)
+ if field_definition is None:
continue
selection_arguments = snake_keys(selection.arguments) if self.auto_snake_case else selection.arguments
diff --git a/src/strawchemy/schema/factories/__init__.py b/src/strawchemy/schema/factories/__init__.py
index d20c7434..7fc16b80 100644
--- a/src/strawchemy/schema/factories/__init__.py
+++ b/src/strawchemy/schema/factories/__init__.py
@@ -3,41 +3,41 @@
from strawchemy.schema.factories.aggregations import AggregationInspector
from strawchemy.schema.factories.base import (
ChildOptions,
- GraphQLDTOFactory,
+ GraphQLFactory,
MappedGraphQLDTOT,
StrawchemyMappedFactory,
- StrawchemyUnMappedDTOFactory,
+ StrawchemyUnMappedFactory,
UnmappedGraphQLDTOT,
)
-from strawchemy.schema.factories.enum import EnumDTOBackend, EnumDTOFactory, UpsertConflictFieldsEnumDTOBackend
-from strawchemy.schema.factories.inputs import AggregateFilterDTOFactory, BooleanFilterDTOFactory, OrderByDTOFactory
+from strawchemy.schema.factories.enum import EnumBackend, EnumFactory, UpsertConflictEnumBackend
+from strawchemy.schema.factories.inputs import AggregateFilterFactory, BooleanFilterFactory, OrderByFactory
from strawchemy.schema.factories.types import (
- AggregateDTOFactory,
- DistinctOnFieldsDTOFactory,
- InputFactory,
- RootAggregateTypeDTOFactory,
- TypeDTOFactory,
- UpsertConflictFieldsDTOFactory,
+ AggregateFieldsFactory,
+ AggregateRootTypeFactory,
+ DistinctOnEnumFactory,
+ MutationInputFactory,
+ ObjectTypeFactory,
+ UpsertConflictEnumFactory,
)
__all__ = (
- "AggregateDTOFactory",
- "AggregateFilterDTOFactory",
+ "AggregateFieldsFactory",
+ "AggregateFilterFactory",
+ "AggregateRootTypeFactory",
"AggregationInspector",
- "BooleanFilterDTOFactory",
+ "BooleanFilterFactory",
"ChildOptions",
- "DistinctOnFieldsDTOFactory",
- "EnumDTOBackend",
- "EnumDTOFactory",
- "GraphQLDTOFactory",
- "InputFactory",
+ "DistinctOnEnumFactory",
+ "EnumBackend",
+ "EnumFactory",
+ "GraphQLFactory",
"MappedGraphQLDTOT",
- "OrderByDTOFactory",
- "RootAggregateTypeDTOFactory",
+ "MutationInputFactory",
+ "ObjectTypeFactory",
+ "OrderByFactory",
"StrawchemyMappedFactory",
- "StrawchemyUnMappedDTOFactory",
- "TypeDTOFactory",
+ "StrawchemyUnMappedFactory",
"UnmappedGraphQLDTOT",
- "UpsertConflictFieldsDTOFactory",
- "UpsertConflictFieldsEnumDTOBackend",
+ "UpsertConflictEnumBackend",
+ "UpsertConflictEnumFactory",
)
diff --git a/src/strawchemy/schema/factories/_kwargs.py b/src/strawchemy/schema/factories/_kwargs.py
new file mode 100644
index 00000000..2ad37a2d
--- /dev/null
+++ b/src/strawchemy/schema/factories/_kwargs.py
@@ -0,0 +1,129 @@
+"""Reusable ``TypedDict``s for factory keyword argument groups.
+
+These exist to collapse the long, repeated kwarg lists across the factory
+methods in this package (`base.py`, `inputs.py`, `enum.py`, `types.py`).
+They are intended to be used with ``typing_extensions.Unpack`` so call
+sites still pass arguments by name and IDE / type checking completion still
+works for individual fields.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Literal
+
+from typing_extensions import TypedDict
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, Mapping, Sequence
+
+ from sqlalchemy.orm import QueryableAttribute
+
+ from strawchemy.dto.base import DTOFieldDefinition
+ from strawchemy.dto.strawberry import BooleanFilterDTO, DTOKey, GraphQLFieldDefinition, OrderByDTO
+ from strawchemy.dto.types import FieldIterable, IncludeFields
+ from strawchemy.schema.pagination import DefaultOffsetPagination
+ from strawchemy.typing import GraphQLPurpose
+
+
+__all__ = (
+ "DTOConfigKwargs",
+ "DecoratorKwargs",
+ "FactoryMethodKwargs",
+ "ForwardedFactoryKwargs",
+ "InputDecoratorKwargs",
+ "MakeInputKwargs",
+ "RegistrationKwargs",
+ "TypeDecoratorKwargs",
+ "TypeWrapperKwargs",
+)
+
+
+class DTOConfigKwargs(TypedDict, total=False):
+ """Args forwarded to ``config(...)`` to build a ``DTOConfig``."""
+
+ include: IncludeFields | None
+ exclude: FieldIterable | None
+ partial: bool | None
+ type_map: Mapping[Any, Any] | None
+ aliases: Mapping[str, str] | None
+ alias_generator: Callable[[str], str] | None
+
+
+class RegistrationKwargs(TypedDict, total=False):
+ """User-facing metadata consumed by ``registry.register_type`` / ``register_enum``.
+
+ ``name`` is intentionally **not** here because it is positional in
+ ``factory()`` and would shadow that parameter when spread via
+ ``Unpack[]``. Methods that take ``name`` as a keyword declare it
+ explicitly.
+
+ Internal-only flags (``register_type``, ``user_defined``) live in
+ ``ForwardedFactoryKwargs`` so they are not exposed via public
+ decorators.
+ """
+
+ description: str | None
+ directives: Sequence[object] | None
+ override: bool
+
+
+class TypeWrapperKwargs(TypedDict, total=False):
+ """Args specific to ``.type()`` / ``_type_wrapper``."""
+
+ paginate: IncludeFields | None
+ default_pagination: DefaultOffsetPagination | None
+ filter_input: type[BooleanFilterDTO] | None
+ distinct_on: IncludeFields | None
+ order: IncludeFields | type[OrderByDTO] | None
+ query_hook: Any
+
+
+class ForwardedFactoryKwargs(TypedDict, total=False):
+ """Pure pass-through args between ``factory()`` overrides.
+
+ Includes internal-only registration flags (``register_type``,
+ ``user_defined``) — these are intentionally not on the public
+ decorators.
+ """
+
+ parent_field_def: DTOFieldDefinition[Any, QueryableAttribute[Any]] | None
+ current_node: Any
+ if_no_fields: Literal["raise", "skip"]
+ tags: set[str] | None
+ backend_kwargs: dict[str, Any] | None
+ no_cache: bool
+ field_map: dict[DTOKey, GraphQLFieldDefinition] | None
+ register_type: bool
+ user_defined: bool
+
+
+class DecoratorKwargs(DTOConfigKwargs, RegistrationKwargs, total=False):
+ """Composite kwargs for plain ``.decorator()`` / ``.input()`` on enum factories."""
+
+
+class TypeDecoratorKwargs(DTOConfigKwargs, RegistrationKwargs, TypeWrapperKwargs, total=False):
+ """Composite kwargs for public ``.type()`` decorator."""
+
+
+class InputDecoratorKwargs(DTOConfigKwargs, RegistrationKwargs, total=False):
+ """Composite kwargs for public ``.input()`` decorator."""
+
+
+class MakeInputKwargs(RegistrationKwargs, ForwardedFactoryKwargs, total=False):
+ """Composite kwargs for ``make_input``."""
+
+ base: type[Any] | None
+
+
+class FactoryMethodKwargs(ForwardedFactoryKwargs, RegistrationKwargs, TypeWrapperKwargs, total=False):
+ """Composite kwargs for ``factory()`` overrides.
+
+ Bundles every kwarg that can flow through a ``factory()`` chain, so
+ that subclass overrides can declare ``**kwargs: Unpack[FactoryMethodKwargs]``
+ and remain Liskov-compatible with siblings that consume type-specific
+ args (paginate, order, etc.) or registration metadata (description,
+ directives) explicitly.
+ """
+
+ mode: GraphQLPurpose | None
+ aggregations: bool
diff --git a/src/strawchemy/schema/factories/aggregations.py b/src/strawchemy/schema/factories/aggregations.py
index a53103a0..9ff56f5a 100644
--- a/src/strawchemy/schema/factories/aggregations.py
+++ b/src/strawchemy/schema/factories/aggregations.py
@@ -4,10 +4,10 @@
from datetime import date, datetime, time, timedelta
from decimal import Decimal
from functools import cached_property
-from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypeVar, cast
+from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, TypeVar, cast
from sqlalchemy.orm import DeclarativeBase
-from typing_extensions import override
+from typing_extensions import Unpack, override
from strawchemy.dto.backend.strawberry import StrawberrryDTOBackend
from strawchemy.dto.strawberry import (
@@ -20,8 +20,8 @@
UnmappedStrawberryGraphQLDTO,
)
from strawchemy.exceptions import DTOError
-from strawchemy.schema.factories.base import GraphQLDTOFactory
-from strawchemy.schema.factories.enum import EnumDTOBackend, EnumDTOFactory
+from strawchemy.schema.factories.base import GraphQLFactory
+from strawchemy.schema.factories.enum import EnumBackend, EnumFactory
if TYPE_CHECKING:
from collections.abc import Generator
@@ -32,6 +32,7 @@
from strawchemy.dto.types import DTOConfig
from strawchemy.mapper import Strawchemy
from strawchemy.repository.typing import DeclarativeT
+ from strawchemy.schema.factories._kwargs import FactoryMethodKwargs
from strawchemy.typing import AggregationFunction, AggregationType, FunctionInfo
from strawchemy.utils.graph import Node
@@ -51,7 +52,7 @@ class _TypeFilterConfig:
types: frozenset[type[Any]] = field(default_factory=frozenset)
-class _CountFieldsDTOFactory(EnumDTOFactory):
+class _CountFieldsFactory(EnumFactory):
@override
def dto_name(
self, base_name: str, dto_config: DTOConfig, node: Node[Relation[Any, EnumDTO], None] | None = None
@@ -59,7 +60,7 @@ def dto_name(
return f"{base_name}CountFields"
-class _FunctionArgDTOFactory(GraphQLDTOFactory[UnmappedStrawberryGraphQLDTO[DeclarativeBase]]):
+class _FunctionArgFactory(GraphQLFactory[UnmappedStrawberryGraphQLDTO[DeclarativeBase]]):
types: ClassVar[set[type[Any]]] = set()
def __init__(
@@ -72,7 +73,7 @@ def __init__(
super().__init__(
mapper, backend or StrawberrryDTOBackend(UnmappedStrawberryGraphQLDTO), handle_cycles, type_map
)
- self._enum_backend = EnumDTOBackend()
+ self._enum_backend = EnumBackend()
@override
def should_exclude_field(
@@ -96,14 +97,14 @@ def iter_field_definitions(
dto_config: DTOConfig,
base: type[DTOBase[DeclarativeBase]] | None,
node: Node[Relation[DeclarativeBase, UnmappedStrawberryGraphQLDTO[DeclarativeBase]], None],
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
*,
field_map: dict[DTOKey, GraphQLFieldDefinition] | None = None,
function: FunctionInfo | None = None,
**kwargs: Any,
) -> Generator[DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]]]:
for field_def in super().iter_field_definitions(
- name, model, dto_config, base, node, raise_if_no_fields, field_map=field_map, **kwargs
+ name, model, dto_config, base, node, if_no_fields, field_map=field_map, **kwargs
):
yield (
FunctionArgFieldDefinition.from_field(field_def, function=function)
@@ -118,28 +119,11 @@ def factory(
dto_config: DTOConfig,
base: type[Any] | None = None,
name: str | None = None,
- parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- current_node: Node[Relation[Any, UnmappedStrawberryGraphQLDTO[DeclarativeBase]], None] | None = None,
- raise_if_no_fields: bool = False,
- tags: set[str] | None = None,
- backend_kwargs: dict[str, Any] | None = None,
*,
function: FunctionInfo | None = None,
- **kwargs: Any,
+ **kwargs: Unpack[FactoryMethodKwargs],
) -> type[UnmappedStrawberryGraphQLDTO[DeclarativeBase]]:
- return super().factory(
- model,
- dto_config,
- base,
- name,
- parent_field_def,
- current_node,
- raise_if_no_fields,
- tags,
- backend_kwargs,
- function=function,
- **kwargs,
- )
+ return super().factory(model, dto_config, base, name, function=function, **kwargs)
def enum_factory(
self,
@@ -147,7 +131,7 @@ def enum_factory(
dto_config: DTOConfig,
name: str | None = None,
base: type[Any] | None = None,
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
**kwargs: Any,
) -> type[EnumDTO]:
if not name:
@@ -158,13 +142,13 @@ def enum_factory(
dto_config=dto_config,
base=base,
node=self._node_or_root(model, name, None),
- raise_if_no_fields=raise_if_no_fields,
+ if_no_fields=if_no_fields,
**kwargs,
)
return self._enum_backend.build(name, model, list(field_defs), base)
-class _TypeFilteredFunctionArgDTOFactory(_FunctionArgDTOFactory):
+class _TypeFilteredFunctionArgFactory(_FunctionArgFactory):
"""Generic factory for type-filtered aggregation field DTOs.
This factory replaces multiple nearly-identical factory classes by using
@@ -190,7 +174,7 @@ def should_exclude_field(
has_override: bool = False,
) -> bool:
return (
- super(_FunctionArgDTOFactory, self).should_exclude_field(field, dto_config, node, has_override)
+ super(_FunctionArgFactory, self).should_exclude_field(field, dto_config, node, has_override)
or field.is_relation
or self.inspector.model_field_type(field) not in self._filter_types
)
@@ -219,11 +203,11 @@ class AggregationInspector:
def __init__(self, mapper: Strawchemy) -> None:
self._inspector = mapper.config.inspector
- self._count_fields_factory = _CountFieldsDTOFactory(self._inspector)
+ self._count_fields_factory = _CountFieldsFactory(mapper)
# Create type-filtered factories from configuration
- self._type_filtered_factories: dict[str, _TypeFilteredFunctionArgDTOFactory] = {
- key: _TypeFilteredFunctionArgDTOFactory(mapper, config)
+ self._type_filtered_factories: dict[str, _TypeFilteredFunctionArgFactory] = {
+ key: _TypeFilteredFunctionArgFactory(mapper, config)
for key, config in self._aggregation_type_filters.items()
}
@@ -342,7 +326,7 @@ def arguments_type(
factory = self._type_filtered_factories.get(aggregation)
if factory is None:
return None
- dto = factory.enum_factory(model, dto_config, raise_if_no_fields=True)
+ dto = factory.enum_factory(model, dto_config, if_no_fields="raise")
except DTOError:
return None
return dto
@@ -352,7 +336,7 @@ def numeric_field_type(
) -> type[UnmappedStrawberryGraphQLDTO[DeclarativeBase]] | None:
try:
factory = self._type_filtered_factories["numeric"]
- dto = factory.factory(model=model, dto_config=dto_config, raise_if_no_fields=True)
+ dto = factory.factory(model=model, dto_config=dto_config, if_no_fields="raise")
except DTOError:
return None
return dto
@@ -362,7 +346,7 @@ def min_max_field_type(
) -> type[UnmappedStrawberryGraphQLDTO[DeclarativeBase]] | None:
try:
factory = self._type_filtered_factories["min_max"]
- dto = factory.factory(model=model, dto_config=dto_config, raise_if_no_fields=True)
+ dto = factory.factory(model=model, dto_config=dto_config, if_no_fields="raise")
except DTOError:
return None
return dto
@@ -372,7 +356,7 @@ def sum_field_type(
) -> type[UnmappedStrawberryGraphQLDTO[DeclarativeBase]] | None:
try:
factory = self._type_filtered_factories["sum"]
- dto = factory.factory(model=model, dto_config=dto_config, raise_if_no_fields=True)
+ dto = factory.factory(model=model, dto_config=dto_config, if_no_fields="raise")
except DTOError:
return None
return dto
diff --git a/src/strawchemy/schema/factories/base.py b/src/strawchemy/schema/factories/base.py
index 8fb7bd99..bd783ded 100644
--- a/src/strawchemy/schema/factories/base.py
+++ b/src/strawchemy/schema/factories/base.py
@@ -22,61 +22,62 @@
from strawberry import UNSET
from strawberry.types.auto import StrawberryAuto
from strawberry.utils.typing import type_has_annotation
-from typing_extensions import dataclass_transform, override
+from typing_extensions import Unpack, dataclass_transform, override
from strawchemy import typing as strawchemy_typing
from strawchemy.dto.base import DTOBackend, DTOBase, DTOFactory, DTOFieldDefinition, Relation
from strawchemy.dto.strawberry import (
BooleanFilterDTO,
DTOKey,
+ EnumDTO,
GraphQLFieldDefinition,
MappedStrawberryGraphQLDTO,
OrderByDTO,
- StrawchemyDTOAttributes,
+ StrawchemyObject,
UnmappedStrawberryGraphQLDTO,
)
-from strawchemy.dto.types import DTOAuto, DTOScope, Purpose
+from strawchemy.dto.types import DTOAuto, DTOConfig, DTOScope, Purpose, is_fields_iterable
from strawchemy.dto.utils import config
-from strawchemy.exceptions import StrawchemyError
+from strawchemy.exceptions import EmptyDTOError, StrawchemyError
from strawchemy.instance import MapperModelInstance
-from strawchemy.schema.pagination import DefaultOffsetPagination
from strawchemy.transpiler import hook
from strawchemy.typing import GraphQLDTOT, GraphQLPurpose, GraphQLType, MappedGraphQLDTO
from strawchemy.utils.annotation import get_annotations
-from strawchemy.utils.registry import RegistryTypeInfo
if TYPE_CHECKING:
from collections.abc import Callable, Generator, Mapping, Sequence
from strawchemy import Strawchemy
from strawchemy.dto.inspectors import SQLAlchemyGraphQLInspector
- from strawchemy.dto.types import DTOConfig, ExcludeFields, IncludeFields
+ from strawchemy.dto.types import FieldIterable, IncludeFields
+ from strawchemy.schema.factories._kwargs import (
+ InputDecoratorKwargs,
+ MakeInputKwargs,
+ TypeDecoratorKwargs,
+ )
+ from strawchemy.schema.pagination import DefaultOffsetPagination
from strawchemy.transpiler.hook import QueryHook
from strawchemy.utils.graph import Node
from strawchemy.validation.pydantic import MappedPydanticGraphQLDTO
-__all__ = ("GraphQLDTOFactory", "StrawchemyMappedFactory", "StrawchemyUnMappedDTOFactory")
+__all__ = ("GraphQLFactory", "StrawchemyMappedFactory", "StrawchemyUnMappedFactory")
T = TypeVar("T", bound="DeclarativeBase")
PydanticGraphQLDTOT = TypeVar("PydanticGraphQLDTOT", bound="MappedPydanticGraphQLDTO[Any]")
MappedGraphQLDTOT = TypeVar("MappedGraphQLDTOT", bound="MappedGraphQLDTO[Any]")
UnmappedGraphQLDTOT = TypeVar("UnmappedGraphQLDTOT", bound="UnmappedStrawberryGraphQLDTO[Any]")
-StrawchemyDTOT = TypeVar("StrawchemyDTOT", bound="StrawchemyDTOAttributes")
+StrawchemyDTOT = TypeVar("StrawchemyDTOT", bound="StrawchemyObject")
TypeScope: TypeAlias = Literal["schema"]
-def type_scope_to_dto_scope(scope: TypeScope) -> DTOScope:
- return "global" if scope == "schema" else "dto"
-
-
@dataclasses.dataclass(eq=True, frozen=True)
class ChildOptions:
pagination: DefaultOffsetPagination | bool = False
order_by: bool = False
-class GraphQLDTOFactory(DTOFactory[DeclarativeBase, QueryableAttribute[Any], GraphQLDTOT]):
+class GraphQLFactory(DTOFactory[DeclarativeBase, QueryableAttribute[Any], GraphQLDTOT]):
inspector: SQLAlchemyGraphQLInspector
def __init__(
@@ -90,61 +91,6 @@ def __init__(
super().__init__(mapper.config.inspector, backend, handle_cycles, type_map, **kwargs)
self._mapper = mapper
- def _type_info(
- self,
- dto: type[StrawchemyDTOT],
- dto_config: DTOConfig,
- current_node: Node[Relation[Any, GraphQLDTOT], None] | None,
- override: bool = False,
- user_defined: bool = False,
- child_options: ChildOptions | None = None,
- ) -> RegistryTypeInfo:
- child_options = child_options or ChildOptions()
- graphql_type = self.graphql_type(dto_config)
- model: type[DeclarativeBase] | None = dto.__dto_model__ if issubclass(dto, MappedStrawberryGraphQLDTO) else None # type: ignore[reportGeneralTypeIssues]
- default_name = self.root_dto_name(model, dto_config, current_node) if model else dto.__name__
- type_info = RegistryTypeInfo(
- name=dto.__name__,
- default_name=default_name,
- graphql_type=graphql_type,
- override=override,
- user_defined=user_defined,
- pagination=DefaultOffsetPagination() if child_options.pagination is True else child_options.pagination,
- order_by=child_options.order_by,
- scope=dto_config.scope,
- model=model,
- exclude_from_scope=dto_config.exclude_from_scope,
- )
- if self._mapper.registry.name_clash(type_info) and current_node is not None:
- type_info = dataclasses.replace(
- type_info, name="".join(node.value.name for node in current_node.path_from_root())
- )
- return type_info
-
- def _register_type(
- self,
- dto: type[StrawchemyDTOT],
- dto_config: DTOConfig,
- current_node: Node[Relation[Any, GraphQLDTOT], None] | None,
- description: str | None = None,
- directives: Sequence[object] | None = (),
- override: bool = False,
- user_defined: bool = False,
- child_options: ChildOptions | None = None,
- ) -> type[StrawchemyDTOT]:
- type_info = self._type_info(
- dto,
- dto_config,
- override=override,
- user_defined=user_defined,
- child_options=child_options,
- current_node=current_node,
- )
- self._raise_if_type_conflicts(type_info)
- return self._mapper.registry.register_type(
- dto, type_info, description=description or dto.__strawchemy_description__, directives=directives
- )
-
def _check_model_instance_attribute(self, base: type[Any]) -> None:
instance_attributes = [
name
@@ -169,19 +115,11 @@ def _resolve_config(self, dto_config: DTOConfig, base: type[Any]) -> DTOConfig:
base.__annotations__ = base_annotations_copy
return config
- def _raise_if_type_conflicts(self, type_info: RegistryTypeInfo) -> None:
- if self._mapper.registry.non_override_exists(type_info):
- msg = (
- f"""Type `{type_info.name}` cannot be auto generated because it's already declared."""
- """ You may want to set `override=True` on the existing type to use it everywhere."""
- )
- raise StrawchemyError(msg)
-
def _config(
self,
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,
@@ -189,33 +127,84 @@ def _config(
scope: DTOScope | None = None,
tags: set[str] | None = None,
) -> DTOConfig:
- return config(
- purpose,
- include=include,
- exclude=exclude,
- partial=partial,
- type_map=type_map,
- alias_generator=alias_generator,
- aliases=aliases,
- scope=scope,
- tags=tags,
+ return (
+ config(
+ purpose,
+ include=include,
+ exclude=exclude,
+ partial=partial,
+ type_map=type_map,
+ alias_generator=alias_generator,
+ aliases=aliases,
+ scope=scope,
+ tags=tags,
+ )
+ | self._mapper.config.field_config
)
+ def _type_order_by(
+ self, model: type[DeclarativeBase], include: IncludeFields | type[OrderByDTO] | None = None
+ ) -> type[OrderByDTO] | None:
+ order_include = self._mapper.config.order_by if include is None else include
+ if is_fields_iterable(order_include) and order_include is not None:
+ try:
+ order_by_input = self._mapper.order_by_factory.make_input(
+ model=model,
+ mode="order_by",
+ dto_config=DTOConfig(
+ Purpose.READ,
+ partial=True,
+ include=order_include,
+ global_include=self._mapper.config.order_by or (),
+ ),
+ if_no_fields="raise",
+ no_cache=True,
+ )
+ except EmptyDTOError:
+ order_by_input = None
+ else:
+ order_by_input = order_include
+ return order_by_input
+
+ def _type_distinct_on(
+ self, model: type[DeclarativeBase], include: IncludeFields | type[EnumDTO] | None = None
+ ) -> type[EnumDTO] | None:
+ distinct_on_include = self._mapper.config.distinct_on if include is None else include
+ if is_fields_iterable(distinct_on_include) and distinct_on_include is not None:
+ try:
+ distinct_on_input = self._mapper.distinct_on_enum_factory.factory(
+ model=model,
+ dto_config=DTOConfig(
+ Purpose.READ,
+ partial=True,
+ include=distinct_on_include,
+ global_include=self._mapper.config.distinct_on or (),
+ ),
+ if_no_fields="raise",
+ no_cache=True,
+ )
+ except EmptyDTOError:
+ distinct_on_input = None
+ else:
+ distinct_on_input = distinct_on_include
+ return distinct_on_input
+
def _type_wrapper(
self,
model: type[T],
*,
mode: GraphQLPurpose,
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,
alias_generator: Callable[[str], str] | None = None,
- child_pagination: bool | DefaultOffsetPagination = False,
- child_order_by: bool = False,
+ paginate: IncludeFields | None = None,
+ distinct_on: IncludeFields | None = None,
+ default_pagination: None | DefaultOffsetPagination = None,
filter_input: type[BooleanFilterDTO] | None = None,
- order_by: type[OrderByDTO] | None = None,
+ order: IncludeFields | type[OrderByDTO] | None = None,
name: str | None = None,
description: str | None = None,
directives: Sequence[object] | None = (),
@@ -225,17 +214,24 @@ def _type_wrapper(
scope: DTOScope | None = None,
) -> Callable[[type[Any]], type[GraphQLDTOT]]:
def wrapper(class_: type[Any]) -> type[GraphQLDTOT]:
- dto_config = config(
- purpose,
- include=include,
- exclude=exclude,
- partial=partial,
- type_map=type_map,
- alias_generator=alias_generator,
- aliases=aliases,
- scope=scope,
- tags={mode},
+ dto_config = (
+ config(
+ purpose,
+ include=include,
+ exclude=exclude,
+ partial=partial,
+ type_map=type_map,
+ alias_generator=alias_generator,
+ aliases=aliases,
+ scope=scope,
+ tags={mode},
+ )
+ | self._mapper.config.field_config
)
+
+ order_by_input = self._type_order_by(model, order)
+ distinct_on_input = self._type_distinct_on(model, distinct_on)
+
dto = self.factory(
model=model,
dto_config=dto_config,
@@ -247,13 +243,21 @@ def wrapper(class_: type[Any]) -> type[GraphQLDTOT]:
override=override,
user_defined=True,
mode=mode,
- child_options=ChildOptions(pagination=child_pagination, order_by=child_order_by),
+ paginate=self._mapper.config.pagination if paginate is None else paginate,
+ order=self._mapper.config.order_by if order is None else order,
+ distinct_on=self._mapper.config.distinct_on if distinct_on is None else distinct_on,
+ default_pagination=default_pagination,
)
- dto.__strawchemy_query_hook__ = query_hook
+ strawchemy_def = dto.__strawchemy_definition__
+ strawchemy_def.query_hook = query_hook
if issubclass(dto, MappedStrawberryGraphQLDTO):
- dto.__strawchemy_filter__ = filter_input
- dto.__strawchemy_order_by__ = order_by
- dto.__strawchemy_purpose__ = mode
+ if order_by_input is not None:
+ strawchemy_def.order_by = order_by_input
+ if distinct_on_input is not None:
+ strawchemy_def.distinct_on = distinct_on_input
+ if filter_input is not None:
+ strawchemy_def.filter = filter_input
+ strawchemy_def.purpose = mode
return dto
return wrapper
@@ -264,7 +268,7 @@ def _input_wrapper(
*,
mode: GraphQLPurpose,
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,
@@ -289,23 +293,37 @@ def wrapper(class_: type[Any]) -> type[GraphQLDTOT]:
scope=scope,
tags={mode},
)
- dto = self.factory(
+ return self.make_input(
model=model,
dto_config=dto_config,
- base=class_,
+ mode=mode,
name=name,
description=description,
directives=directives,
override=override,
- user_defined=True,
- mode=mode,
+ base=class_,
**kwargs,
)
- dto.__strawchemy_purpose__ = mode
- return dto
return wrapper
+ @classmethod
+ def _type_scope_to_dto_scope(cls, scope: TypeScope) -> DTOScope:
+ return "global" if scope == "schema" else "dto"
+
+ def make_input(
+ self,
+ model: type[T],
+ *,
+ mode: GraphQLPurpose,
+ dto_config: DTOConfig,
+ name: str | None = None,
+ **kwargs: Unpack[MakeInputKwargs],
+ ) -> type[GraphQLDTOT]:
+ dto = self.factory(model=model, dto_config=dto_config, name=name, mode=mode, **kwargs)
+ dto.__strawchemy_definition__.purpose = mode
+ return dto
+
@cached_property
def _namespace(self) -> dict[str, Any]:
return vars(strawchemy_typing) | vars(hook)
@@ -329,13 +347,13 @@ def iter_field_definitions(
dto_config: DTOConfig,
base: type[DTOBase[DeclarativeBase]] | None,
node: Node[Relation[DeclarativeBase, GraphQLDTOT], None],
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
*,
field_map: dict[DTOKey, GraphQLFieldDefinition] | None = None,
**kwargs: Any,
) -> Generator[DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]]]:
field_map = field_map if field_map is not None else {}
- for field in super().iter_field_definitions(name, model, dto_config, base, node, raise_if_no_fields, **kwargs):
+ for field in super().iter_field_definitions(name, model, dto_config, base, node, if_no_fields, **kwargs):
key = DTOKey.from_dto_node(node)
graphql_field = GraphQLFieldDefinition.from_field(field)
yield graphql_field
@@ -348,12 +366,13 @@ def factory(
dto_config: DTOConfig,
base: type[Any] | None = None,
name: str | None = None,
+ *,
parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
current_node: Node[Relation[Any, GraphQLDTOT], None] | None = None,
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
tags: set[str] | None = None,
backend_kwargs: dict[str, Any] | None = None,
- *,
+ no_cache: bool = False,
description: str | None = None,
directives: Sequence[object] | None = (),
override: bool = False,
@@ -362,39 +381,47 @@ def factory(
**kwargs: Any,
) -> type[GraphQLDTOT]:
field_map: dict[DTOKey, GraphQLFieldDefinition] = {}
+ if not user_defined and no_cache:
+ name = self.root_dto_name(model, dto_config, current_node) if name is None else name
+ name = self._mapper.registry.uniquify_name(self.graphql_type(dto_config), name)
if base:
self._check_model_instance_attribute(base)
dto_config = self._resolve_config(dto_config, base)
- dto = super().factory(
+
+ dto: type[GraphQLDTOT] = super().factory(
model,
dto_config,
base,
name,
- parent_field_def,
- current_node,
- raise_if_no_fields,
- tags,
+ parent_field_def=parent_field_def,
+ current_node=current_node,
+ if_no_fields=if_no_fields,
+ tags=tags,
backend_kwargs=backend_kwargs,
+ no_cache=no_cache,
field_map=field_map,
**kwargs,
)
- if not dto.__strawchemy_field_map__:
- dto.__strawchemy_field_map__ = field_map
- dto.__strawchemy_description__ = self.type_description()
+ if not dto.__strawchemy_definition__.field_map:
+ dto.__strawchemy_definition__.field_map = field_map
+ dto.__strawchemy_definition__.description = self.type_description()
+
if register_type:
- return self._register_type(
+ return self._mapper.registry.register_type(
dto,
- dto_config,
+ graphql_type=self.graphql_type(dto_config),
+ dto_config=dto_config,
current_node=current_node,
description=description,
directives=directives,
override=override,
user_defined=user_defined,
+ default_name=self.root_dto_name(model, dto_config),
)
return dto
-class StrawchemyMappedFactory(GraphQLDTOFactory[MappedGraphQLDTOT]):
+class StrawchemyMappedFactory(GraphQLFactory[MappedGraphQLDTOT]):
def _root_input_config(self, model: type[Any], dto_config: DTOConfig, mode: GraphQLPurpose) -> DTOConfig:
annotations_overrides: dict[str, Any] = {}
partial = dto_config.partial
@@ -430,45 +457,19 @@ def type(
self,
model: type[T],
*,
- include: IncludeFields | None = None,
- exclude: ExcludeFields | None = None,
- partial: bool | None = None,
- type_map: Mapping[Any, Any] | None = None,
- aliases: Mapping[str, str] | None = None,
- alias_generator: Callable[[str], str] | None = None,
- child_pagination: bool | DefaultOffsetPagination = False,
- child_order_by: bool = False,
- filter_input: type[BooleanFilterDTO] | None = None,
- order_by: type[OrderByDTO] | None = None,
name: str | None = None,
- description: str | None = None,
- directives: Sequence[object] | None = (),
- query_hook: QueryHook[T] | list[QueryHook[T]] | None = None,
- override: bool = False,
purpose: Purpose = Purpose.READ,
scope: TypeScope | None = None,
mode: GraphQLPurpose = "type",
+ **kwargs: Unpack[TypeDecoratorKwargs],
) -> Callable[[type[Any]], type[MappedGraphQLDTO[T]]]:
return self._type_wrapper(
model=model,
- include=include,
- exclude=exclude,
- partial=partial,
- type_map=type_map,
- aliases=aliases,
- alias_generator=alias_generator,
- child_pagination=child_pagination,
- child_order_by=child_order_by,
- filter_input=filter_input,
- order_by=order_by,
name=name,
- description=description,
- directives=directives,
- query_hook=query_hook,
- override=override,
purpose=purpose,
- scope=type_scope_to_dto_scope(scope) if scope else None,
+ scope=self._type_scope_to_dto_scope(scope) if scope else None,
mode=mode,
+ **kwargs,
)
@dataclass_transform(order_default=True, kw_only_default=True)
@@ -477,35 +478,17 @@ def input(
model: type[T],
*,
mode: GraphQLPurpose,
- include: IncludeFields | None = None,
- exclude: ExcludeFields | None = None,
- partial: bool | None = None,
- type_map: Mapping[Any, Any] | None = None,
- aliases: Mapping[str, str] | None = None,
- alias_generator: Callable[[str], str] | None = None,
name: str | None = None,
- description: str | None = None,
- directives: Sequence[object] | None = (),
- override: bool = False,
purpose: Purpose = Purpose.WRITE,
scope: TypeScope | None = None,
- **kwargs: Any,
+ **kwargs: Unpack[InputDecoratorKwargs],
) -> Callable[[type[Any]], type[MappedGraphQLDTO[T]]]:
return self._input_wrapper(
model=model,
- include=include,
- exclude=exclude,
- partial=partial,
- type_map=type_map,
- aliases=aliases,
- alias_generator=alias_generator,
name=name,
- description=description,
- directives=directives,
- override=override,
purpose=purpose,
mode=mode,
- scope=type_scope_to_dto_scope(scope) if scope else None,
+ scope=self._type_scope_to_dto_scope(scope) if scope else None,
**kwargs,
)
@@ -516,106 +499,37 @@ def factory(
dto_config: DTOConfig,
base: type[Any] | None = None,
name: str | None = None,
- parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- current_node: Node[Relation[Any, MappedGraphQLDTOT], None] | None = None,
- raise_if_no_fields: bool = False,
- tags: set[str] | None = None,
- backend_kwargs: dict[str, Any] | None = None,
*,
mode: GraphQLPurpose | None = None,
**kwargs: Any,
) -> type[MappedGraphQLDTOT]:
if mode and dto_config.purpose is Purpose.WRITE:
dto_config = self._root_input_config(model, dto_config, mode)
- return super().factory(
- model,
- dto_config,
- base,
- name,
- parent_field_def,
- current_node,
- raise_if_no_fields,
- tags,
- backend_kwargs=backend_kwargs,
- mode=mode,
- **kwargs,
- )
+ return super().factory(model, dto_config, base, name, mode=mode, **kwargs)
-class StrawchemyUnMappedDTOFactory(GraphQLDTOFactory[UnmappedGraphQLDTOT]):
+class StrawchemyUnMappedFactory(GraphQLFactory[UnmappedGraphQLDTOT]):
@dataclass_transform(order_default=True, kw_only_default=True)
def input(
self,
model: type[T],
*,
- include: IncludeFields | None = None,
- exclude: ExcludeFields | None = None,
- partial: bool | None = None,
- type_map: Mapping[Any, Any] | None = None,
- aliases: Mapping[str, str] | None = None,
- alias_generator: Callable[[str], str] | None = None,
+ mode: GraphQLPurpose = "create_input",
name: str | None = None,
- description: str | None = None,
- directives: Sequence[object] | None = (),
- override: bool = False,
purpose: Purpose = Purpose.WRITE,
- **kwargs: Any,
+ scope: TypeScope | None = None,
+ **kwargs: Unpack[InputDecoratorKwargs],
) -> Callable[[type[Any]], type[UnmappedStrawberryGraphQLDTO[T]]]:
- return self._input_wrapper(
- model=model,
- include=include,
- exclude=exclude,
- partial=partial,
- type_map=type_map,
- aliases=aliases,
- alias_generator=alias_generator,
- name=name,
- description=description,
- directives=directives,
- override=override,
- purpose=purpose,
- **kwargs,
- )
+ return self._input_wrapper(model=model, mode=mode, name=name, purpose=purpose, **kwargs)
@dataclass_transform(order_default=True, kw_only_default=True)
def type(
self,
model: type[T],
- include: IncludeFields | None = None,
- exclude: ExcludeFields | None = None,
- partial: bool | None = None,
- type_map: Mapping[Any, Any] | None = None,
- aliases: Mapping[str, str] | None = None,
- alias_generator: Callable[[str], str] | None = None,
- child_pagination: bool | DefaultOffsetPagination = False,
- child_order_by: bool = False,
- filter_input: type[BooleanFilterDTO] | None = None,
- order_by: type[OrderByDTO] | None = None,
+ *,
name: str | None = None,
- description: str | None = None,
- directives: Sequence[object] | None = (),
- query_hook: QueryHook[T] | list[QueryHook[T]] | None = None,
- override: bool = False,
purpose: Purpose = Purpose.READ,
mode: GraphQLPurpose = "type",
+ **kwargs: Unpack[TypeDecoratorKwargs],
) -> Callable[[type[Any]], type[UnmappedStrawberryGraphQLDTO[T]]]:
- return self._type_wrapper(
- model=model,
- include=include,
- exclude=exclude,
- partial=partial,
- type_map=type_map,
- aliases=aliases,
- alias_generator=alias_generator,
- child_pagination=child_pagination,
- child_order_by=child_order_by,
- filter_input=filter_input,
- order_by=order_by,
- name=name,
- description=description,
- directives=directives,
- query_hook=query_hook,
- override=override,
- purpose=purpose,
- mode=mode,
- )
+ return self._type_wrapper(model=model, name=name, purpose=purpose, mode=mode, **kwargs)
diff --git a/src/strawchemy/schema/factories/enum.py b/src/strawchemy/schema/factories/enum.py
index 05d445ab..61c76c49 100644
--- a/src/strawchemy/schema/factories/enum.py
+++ b/src/strawchemy/schema/factories/enum.py
@@ -3,26 +3,28 @@
from enum import Enum
from inspect import getmodule
from types import new_class
-from typing import TYPE_CHECKING, Any, TypeVar, cast
+from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
from sqlalchemy.orm import DeclarativeBase, QueryableAttribute
-from typing_extensions import override
+from typing_extensions import Unpack, override
from strawchemy.dto.base import DTOBackend, DTOBase, DTOFactory, DTOFieldDefinition, Relation
from strawchemy.dto.strawberry import EnumDTO, GraphQLFieldDefinition
-from strawchemy.dto.types import DTOConfig, ExcludeFields, IncludeFields, Purpose
+from strawchemy.dto.types import DTOConfig, Purpose
from strawchemy.utils.text import snake_to_lower_camel_case
if TYPE_CHECKING:
- from collections.abc import Callable, Generator, Iterable, Mapping
+ from collections.abc import Callable, Generator, Iterable
+ from strawchemy import Strawchemy
from strawchemy.dto.inspectors import SQLAlchemyGraphQLInspector
+ from strawchemy.schema.factories._kwargs import DecoratorKwargs, FactoryMethodKwargs
from strawchemy.utils.graph import Node
T = TypeVar("T")
-class EnumDTOBackend(DTOBackend[EnumDTO]):
+class EnumBackend(DTOBackend[EnumDTO]):
def __init__(self, to_camel: bool = True) -> None:
self.dto_base = EnumDTO
self.to_camel = to_camel
@@ -66,7 +68,7 @@ def copy(cls, dto: type[EnumDTO], name: str) -> EnumDTO: # pyright: ignore[repo
return enum
-class UpsertConflictFieldsEnumDTOBackend(EnumDTOBackend):
+class UpsertConflictEnumBackend(EnumBackend):
def __init__(self, inspector: SQLAlchemyGraphQLInspector, to_camel: bool = True) -> None:
self.dto_base = EnumDTO
self.to_camel = to_camel
@@ -93,17 +95,18 @@ def build(
)
-class EnumDTOFactory(DTOFactory[DeclarativeBase, QueryableAttribute[Any], EnumDTO]):
+class EnumFactory(DTOFactory[DeclarativeBase, QueryableAttribute[Any], EnumDTO]):
inspector: SQLAlchemyGraphQLInspector
def __init__(
self,
- inspector: SQLAlchemyGraphQLInspector,
+ mapper: Strawchemy,
backend: DTOBackend[EnumDTO] | None = None,
handle_cycles: bool = True,
type_map: dict[Any, Any] | None = None,
) -> None:
- super().__init__(inspector, backend or EnumDTOBackend(), handle_cycles, type_map)
+ self._mapper = mapper
+ super().__init__(mapper.config.inspector, backend or EnumBackend(), handle_cycles, type_map)
@override
def dto_name(
@@ -129,10 +132,10 @@ def iter_field_definitions(
dto_config: DTOConfig,
base: type[DTOBase[DeclarativeBase]] | None,
node: Node[Relation[DeclarativeBase, EnumDTO], None],
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
**kwargs: Any,
) -> Generator[DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]]]:
- for field in super().iter_field_definitions(name, model, dto_config, base, node, raise_if_no_fields, **kwargs):
+ for field in super().iter_field_definitions(name, model, dto_config, base, node, if_no_fields, **kwargs):
yield GraphQLFieldDefinition.from_field(field)
@override
@@ -140,48 +143,16 @@ def decorator(
self,
model: type[DeclarativeBase],
purpose: Purpose = Purpose.READ,
- include: IncludeFields | None = None,
- exclude: ExcludeFields | None = None,
- partial: bool | None = None,
- type_map: Mapping[Any, Any] | None = None,
- aliases: Mapping[str, str] | None = None,
- alias_generator: Callable[[str], str] | None = None,
- **kwargs: Any,
+ **kwargs: Unpack[DecoratorKwargs],
) -> Callable[[type[Any]], type[EnumDTO]]:
- return super().decorator(
- model,
- purpose,
- include=include,
- exclude=exclude,
- partial=partial,
- aliases=aliases,
- alias_generator=alias_generator,
- type_map=type_map,
- **kwargs,
- )
+ return super().decorator(model, purpose, **kwargs)
def input(
self,
model: type[DeclarativeBase],
- include: IncludeFields | None = None,
- exclude: ExcludeFields | None = None,
- partial: bool | None = None,
- type_map: Mapping[Any, Any] | None = None,
- aliases: Mapping[str, str] | None = None,
- alias_generator: Callable[[str], str] | None = None,
- **kwargs: Any,
+ **kwargs: Unpack[DecoratorKwargs],
) -> Callable[[type[Any]], type[EnumDTO]]:
- return super().decorator(
- model,
- Purpose.WRITE,
- include=include,
- exclude=exclude,
- partial=partial,
- aliases=aliases,
- alias_generator=alias_generator,
- type_map=type_map,
- **kwargs,
- )
+ return super().decorator(model, Purpose.WRITE, **kwargs)
def upsert_conflict_fields(
self,
@@ -199,3 +170,25 @@ def upsert_conflict_fields(
],
),
)
+
+ @override
+ def factory(
+ self,
+ model: type[DeclarativeBase],
+ dto_config: DTOConfig,
+ base: type[Any] | None = None,
+ name: str | None = None,
+ **kwargs: Unpack[FactoryMethodKwargs],
+ ) -> type[EnumDTO]:
+ register_type = kwargs.get("register_type", True)
+ dto = super().factory(model=model, dto_config=dto_config, base=base, name=name, **kwargs)
+ if register_type:
+ return self._mapper.registry.register_enum(
+ dto,
+ dto_config=dto_config,
+ description=kwargs.get("description"),
+ directives=kwargs.get("directives") or (),
+ override=kwargs.get("override", False),
+ user_defined=kwargs.get("user_defined", False),
+ )
+ return dto
diff --git a/src/strawchemy/schema/factories/inputs.py b/src/strawchemy/schema/factories/inputs.py
index 02869d39..6ba8a957 100644
--- a/src/strawchemy/schema/factories/inputs.py
+++ b/src/strawchemy/schema/factories/inputs.py
@@ -1,9 +1,9 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
+from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
from strawberry import UNSET
-from typing_extensions import override
+from typing_extensions import Unpack, override
from strawchemy.dto.backend.strawberry import StrawberrryDTOBackend
from strawchemy.dto.strawberry import (
@@ -20,81 +20,62 @@
OrderByEnum,
)
from strawchemy.dto.types import DTOConfig, DTOMissing, Purpose
-from strawchemy.schema.factories import AggregationInspector, StrawchemyUnMappedDTOFactory, UnmappedGraphQLDTOT
+from strawchemy.schema.factories import AggregationInspector, StrawchemyUnMappedFactory, UnmappedGraphQLDTOT
from strawchemy.typing import AggregationFunction, GraphQLFilterDTOT, GraphQLPurpose, GraphQLType
-from strawchemy.utils.registry import RegistryTypeInfo
from strawchemy.utils.text import snake_to_camel
if TYPE_CHECKING:
- from collections.abc import Callable, Generator, Mapping, Sequence
+ from collections.abc import Callable, Generator
from sqlalchemy.orm import DeclarativeBase, QueryableAttribute
from strawchemy import Strawchemy
from strawchemy.dto.base import DTOBackend, DTOBase, DTOFieldDefinition, ModelFieldT, Relation
- from strawchemy.dto.types import ExcludeFields, IncludeFields
from strawchemy.repository.typing import DeclarativeT
+ from strawchemy.schema.factories._kwargs import FactoryMethodKwargs, InputDecoratorKwargs
+ from strawchemy.schema.factories.base import TypeScope
from strawchemy.schema.filters import GraphQLFilter
from strawchemy.utils.graph import Node
T = TypeVar("T")
-class _BaseStrawchemyFilterFactory(StrawchemyUnMappedDTOFactory[UnmappedGraphQLDTOT]):
+class _BaseFilterFactory(StrawchemyUnMappedFactory[UnmappedGraphQLDTOT]):
@classmethod
@override
def graphql_type(cls, dto_config: DTOConfig) -> GraphQLType:
return "input"
+ @override
+ def type_description(self) -> str:
+ return "GraphQL Filter Input"
+
@override
def input(
self,
model: type[DeclarativeT],
*,
- include: IncludeFields | None = None,
- exclude: ExcludeFields | None = None,
- partial: bool | None = None,
- type_map: Mapping[Any, Any] | None = None,
- aliases: Mapping[str, str] | None = None,
- alias_generator: Callable[[str], str] | None = None,
name: str | None = None,
- description: str | None = None,
- directives: Sequence[object] | None = (),
- override: bool = False,
purpose: Purpose = Purpose.READ,
mode: GraphQLPurpose = "filter",
- **kwargs: Any,
+ scope: TypeScope | None = None,
+ **kwargs: Unpack[InputDecoratorKwargs],
) -> Callable[[type[Any]], type[UnmappedGraphQLDTOT]]:
- return self._input_wrapper(
- model=model,
- include=include,
- exclude=exclude,
- partial=partial,
- type_map=type_map,
- aliases=aliases,
- alias_generator=alias_generator,
- name=name,
- description=description,
- directives=directives,
- override=override,
- purpose=purpose,
- mode=mode,
- **kwargs,
- )
+ return self._input_wrapper(model=model, name=name, purpose=purpose, mode=mode, **kwargs)
-class _FilterDTOFactory(_BaseStrawchemyFilterFactory[GraphQLFilterDTOT]):
+class _FilterFactory(_BaseFilterFactory[GraphQLFilterDTOT]):
def __init__(
self,
mapper: Strawchemy,
backend: DTOBackend[GraphQLFilterDTOT],
handle_cycles: bool = True,
type_map: dict[Any, Any] | None = None,
- aggregation_filter_factory: AggregateFilterDTOFactory | None = None,
+ aggregation_filter_factory: AggregateFilterFactory | None = None,
**kwargs: Any,
) -> None:
super().__init__(mapper, backend, handle_cycles, type_map, **kwargs)
- self._aggregation_filter_factory = aggregation_filter_factory or AggregateFilterDTOFactory(mapper)
+ self._aggregation_filter_factory = aggregation_filter_factory or AggregateFilterFactory(mapper)
def _filter_type(self, field: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]]) -> type[GraphQLFilter]:
return self.inspector.get_field_comparison(field)
@@ -115,10 +96,6 @@ def _aggregation_field(
default=UNSET,
)
- @override
- def type_description(self) -> str:
- return "Boolean expression to compare fields. All fields are combined with logical 'AND'."
-
@override
def iter_field_definitions(
self,
@@ -127,7 +104,7 @@ def iter_field_definitions(
dto_config: DTOConfig,
base: type[DTOBase[DeclarativeBase]] | None,
node: Node[Relation[DeclarativeBase, GraphQLFilterDTOT], None],
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
*,
aggregate_filters: bool = False,
field_map: dict[DTOKey, GraphQLFieldDefinition] | None = None,
@@ -135,7 +112,7 @@ def iter_field_definitions(
) -> Generator[DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]]]:
field_map = field_map if field_map is not None else {}
for field in super().iter_field_definitions(
- name, model, dto_config, base, node, raise_if_no_fields, field_map=field_map, **kwargs
+ name, model, dto_config, base, node, if_no_fields, field_map=field_map, **kwargs
):
key = DTOKey.from_dto_node(node)
if field.is_relation:
@@ -143,7 +120,9 @@ def iter_field_definitions(
if field.uselist and field.related_dto:
field.type_ = Union[field.related_dto, None]
if aggregate_filters:
- aggregation_field = self._aggregation_field(field, dto_config.copy_with(partial_default=UNSET))
+ aggregation_field = self._aggregation_field(
+ field, dto_config.copy_with(partial_default=UNSET, partial=True)
+ )
field_map[key + aggregation_field.name] = aggregation_field
yield aggregation_field
else:
@@ -167,38 +146,21 @@ def factory(
dto_config: DTOConfig,
base: type[Any] | None = None,
name: str | None = None,
- parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- current_node: Node[Relation[Any, GraphQLFilterDTOT], None] | None = None,
- raise_if_no_fields: bool = False,
- tags: set[str] | None = None,
- backend_kwargs: dict[str, Any] | None = None,
*,
aggregate_filters: bool = True,
- **kwargs: Any,
+ **kwargs: Unpack[FactoryMethodKwargs],
) -> type[GraphQLFilterDTOT]:
- return super().factory(
- model,
- dto_config,
- base,
- name,
- parent_field_def,
- current_node,
- raise_if_no_fields,
- tags,
- backend_kwargs,
- aggregate_filters=aggregate_filters,
- **kwargs,
- )
+ return super().factory(model, dto_config, base, name, aggregate_filters=aggregate_filters, **kwargs)
-class BooleanFilterDTOFactory(_FilterDTOFactory[BooleanFilterDTO]):
+class BooleanFilterFactory(_FilterFactory[BooleanFilterDTO]):
def __init__(
self,
mapper: Strawchemy,
backend: DTOBackend[BooleanFilterDTO] | None = None,
handle_cycles: bool = True,
type_map: dict[Any, Any] | None = None,
- aggregate_filter_factory: AggregateFilterDTOFactory | None = None,
+ aggregate_filter_factory: AggregateFilterFactory | None = None,
**kwargs: Any,
) -> None:
super().__init__(
@@ -210,8 +172,12 @@ def __init__(
**kwargs,
)
+ @override
+ def type_description(self) -> str:
+ return "Boolean expression to compare fields. All fields are combined with logical 'AND'."
+
-class AggregateFilterDTOFactory(_BaseStrawchemyFilterFactory[AggregateFilterDTO]):
+class AggregateFilterFactory(_BaseFilterFactory[AggregateFilterDTO]):
def __init__(
self,
mapper: Strawchemy,
@@ -281,16 +247,18 @@ def _aggregate_function_type(
),
],
)
- key = DTOKey([model])
- dto.__strawchemy_field_map__ = {
- key + name: FunctionArgFieldDefinition.from_field(field, function=aggregation)
- for name, field in self.inspector.field_definitions(model, dto_config)
- }
- dto.__strawchemy_description__ = "Field filtering information"
+ fields = [
+ FunctionArgFieldDefinition.from_field(field, function=aggregation)
+ for _, field in self.inspector.field_definitions(model, dto_config)
+ ]
+ dto.__strawchemy_definition__.populate_fields(model, fields)
+ dto.__strawchemy_definition__.description = "Field filtering information"
dto.__dto_function_info__ = aggregation
return self._mapper.registry.register_type(
dto,
- RegistryTypeInfo(dto.__name__, "input", default_name=self.root_dto_name(model, dto_config)),
+ dto_config=dto_config,
+ graphql_type="input",
+ default_name=self.root_dto_name(model, dto_config),
description=f"Boolean expression to compare {aggregation.function} aggregation.",
)
@@ -303,7 +271,7 @@ def _factory(
node: Node[Relation[Any, AggregateFilterDTO], None],
base: type[Any] | None = None,
parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
backend_kwargs: dict[str, Any] | None = None,
**kwargs: Any,
) -> type[AggregateFilterDTO]:
@@ -332,23 +300,22 @@ def _factory(
_function=aggregation,
),
)
- key = DTOKey([model])
dto = self.backend.build(name, model, field_defs, **(backend_kwargs or {}))
- dto.__strawchemy_description__ = (
+ dto.__strawchemy_definition__.description = (
"Boolean expression to compare field aggregations. All fields are combined with logical 'AND'."
)
- dto.__strawchemy_field_map__ = {key + field.name: field for field in field_defs}
+ dto.__strawchemy_definition__.populate_fields(model, field_defs)
return dto
-class OrderByDTOFactory(_FilterDTOFactory[OrderByDTO]):
+class OrderByFactory(_FilterFactory[OrderByDTO]):
def __init__(
self,
mapper: Strawchemy,
backend: DTOBackend[OrderByDTO] | None = None,
handle_cycles: bool = True,
type_map: dict[Any, Any] | None = None,
- aggregation_filter_factory: AggregateFilterDTOFactory | None = None,
+ aggregation_filter_factory: AggregateFilterFactory | None = None,
) -> None:
super().__init__(
mapper,
@@ -358,6 +325,10 @@ def __init__(
aggregation_filter_factory,
)
+ @override
+ def type_description(self) -> str:
+ return "Ordering input."
+
@override
def _filter_type(self, field: DTOFieldDefinition[T, ModelFieldT]) -> type[OrderByEnum]:
return OrderByEnum
@@ -378,13 +349,16 @@ def _order_by_aggregation_fields(
name = f"{model.__name__}Aggregate{snake_to_camel(aggregation.aggregation_type)}FieldsOrderBy"
dto = self.backend.build(name, model, field_defs)
- key = DTOKey([model])
- dto.__strawchemy_field_map__ = {
- key + name: FunctionArgFieldDefinition.from_field(field, function=aggregation)
- for name, field in self.inspector.field_definitions(model, dto_config)
- }
+ fields = [
+ FunctionArgFieldDefinition.from_field(field, function=aggregation)
+ for _, field in self.inspector.field_definitions(model, dto_config)
+ ]
+ dto.__strawchemy_definition__.populate_fields(model, fields)
return self._mapper.registry.register_type(
- dto, RegistryTypeInfo(dto.__name__, "input", default_name=self.root_dto_name(model, dto_config))
+ dto,
+ dto_config=dto_config,
+ graphql_type="input",
+ default_name=self.root_dto_name(model, dto_config),
)
def _order_by_aggregation(self, model: type[DeclarativeBase], dto_config: DTOConfig) -> type[OrderByDTO]:
@@ -412,9 +386,12 @@ def _order_by_aggregation(self, model: type[DeclarativeBase], dto_config: DTOCon
)
dto = self.backend.build(f"{model.__name__}AggregateOrderBy", model, field_definitions)
- dto.__strawchemy_field_map__ = {DTOKey([model, field.name]): field for field in field_definitions}
+ dto.__strawchemy_definition__.populate_fields(model, field_definitions)
return self._mapper.registry.register_type(
- dto, RegistryTypeInfo(dto.__name__, "input", default_name=self.root_dto_name(model, dto_config))
+ dto,
+ dto_config=dto_config,
+ graphql_type="input",
+ default_name=self.root_dto_name(model, dto_config),
)
@override
@@ -431,6 +408,16 @@ def _aggregation_field(
default=UNSET,
)
+ @override
+ def _resolve_relation_type(
+ self,
+ field: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]],
+ dto_config: DTOConfig,
+ node: Node[Relation[Any, OrderByDTO], None],
+ **factory_kwargs: Any,
+ ) -> Any:
+ return super()._resolve_relation_type(field, dto_config.copy_with(include="all"), node, **factory_kwargs)
+
@override
def dto_name(
self,
@@ -447,27 +434,10 @@ def factory(
dto_config: DTOConfig,
base: type[Any] | None = None,
name: str | None = None,
- parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- current_node: Node[Relation[Any, OrderByDTO], None] | None = None,
- raise_if_no_fields: bool = False,
- tags: set[str] | None = None,
- backend_kwargs: dict[str, Any] | None = None,
*,
aggregate_filters: bool = True,
- **kwargs: Any,
+ **kwargs: Unpack[FactoryMethodKwargs],
) -> type[OrderByDTO]:
- dto = super().factory(
- model,
- dto_config,
- base,
- name,
- parent_field_def,
- current_node,
- raise_if_no_fields,
- tags,
- backend_kwargs,
- aggregate_filters=aggregate_filters,
- **kwargs,
- )
- dto.__strawchemy_description__ = "Ordering options"
+ dto = super().factory(model, dto_config, base, name, aggregate_filters=aggregate_filters, **kwargs)
+ dto.__strawchemy_definition__.description = "Ordering options"
return dto
diff --git a/src/strawchemy/schema/factories/types.py b/src/strawchemy/schema/factories/types.py
index 861325bd..9a32a00d 100644
--- a/src/strawchemy/schema/factories/types.py
+++ b/src/strawchemy/schema/factories/types.py
@@ -1,12 +1,12 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
+from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
from sqlalchemy import JSON
from sqlalchemy.orm import DeclarativeBase, QueryableAttribute
from strawberry.annotation import StrawberryAnnotation
from strawberry.types.arguments import StrawberryArgument
-from typing_extensions import Self, override
+from typing_extensions import Self, Unpack, override
from strawchemy.constants import AGGREGATIONS_KEY, JSON_PATH_KEY, NODES_KEY
from strawchemy.dto.backend.strawberry import StrawberrryDTOBackend
@@ -19,19 +19,19 @@
FunctionFieldDefinition,
GraphQLFieldDefinition,
MappedStrawberryGraphQLDTO,
+ OrderByDTO,
)
-from strawchemy.dto.types import DTOConfig, DTOMissing, Purpose
-from strawchemy.dto.utils import read_all_partial_config, read_partial, write_all_config
+from strawchemy.dto.types import DTOConfig, DTOMissing, IncludeFields, Purpose, is_fields_iterable
+from strawchemy.dto.utils import read_partial, write_all_config
from strawchemy.exceptions import EmptyDTOError
from strawchemy.schema.factories import (
AggregationInspector,
- ChildOptions,
- EnumDTOFactory,
- GraphQLDTOFactory,
+ EnumFactory,
+ GraphQLFactory,
MappedGraphQLDTOT,
- OrderByDTOFactory,
+ OrderByFactory,
StrawchemyMappedFactory,
- UpsertConflictFieldsEnumDTOBackend,
+ UpsertConflictEnumBackend,
)
from strawchemy.schema.mutation import (
RequiredToManyUpdateInput,
@@ -45,47 +45,54 @@
from strawchemy.utils.text import snake_to_camel
if TYPE_CHECKING:
- from collections.abc import Generator, Hashable, Sequence
+ from collections.abc import Generator, Hashable
from enum import Enum
+ from strawberry.types.field import StrawberryField
+
from strawchemy import Strawchemy
from strawchemy.dto.base import DTOBackend, DTOBase, Relation
from strawchemy.dto.inspectors import SQLAlchemyGraphQLInspector
from strawchemy.repository.typing import DeclarativeT
+ from strawchemy.schema.factories._kwargs import FactoryMethodKwargs
from strawchemy.schema.pagination import DefaultOffsetPagination
from strawchemy.utils.graph import Node
__all__ = (
- "AggregateDTOFactory",
- "DistinctOnFieldsDTOFactory",
- "InputFactory",
- "RootAggregateTypeDTOFactory",
- "TypeDTOFactory",
- "UpsertConflictFieldsDTOFactory",
+ "AggregateFieldsFactory",
+ "AggregateRootTypeFactory",
+ "DistinctOnEnumFactory",
+ "MutationInputFactory",
+ "ObjectTypeFactory",
+ "UpsertConflictEnumFactory",
)
T = TypeVar("T")
-class TypeDTOFactory(StrawchemyMappedFactory[MappedGraphQLDTOT]):
+class ObjectTypeFactory(StrawchemyMappedFactory[MappedGraphQLDTOT]):
def __init__(
self,
mapper: Strawchemy,
backend: DTOBackend[MappedGraphQLDTOT],
handle_cycles: bool = True,
type_map: dict[Any, Any] | None = None,
- aggregation_factory: AggregateDTOFactory[AggregateDTOT] | None = None,
- order_by_factory: OrderByDTOFactory | None = None,
+ aggregation_factory: AggregateFieldsFactory[AggregateDTOT] | None = None,
+ order_by_factory: OrderByFactory | None = None,
+ distinct_on_factory: DistinctOnEnumFactory | None = None,
**kwargs: Any,
) -> None:
super().__init__(mapper, backend, handle_cycles, type_map, **kwargs)
- self._aggregation_factory = aggregation_factory or AggregateDTOFactory(
+ self._aggregation_factory = aggregation_factory or AggregateFieldsFactory(
mapper, StrawberrryDTOBackend(AggregateDTO)
)
- self._order_by_factory = order_by_factory or OrderByDTOFactory(
+ self._order_by_factory = order_by_factory or OrderByFactory(
mapper, handle_cycles=handle_cycles, type_map=type_map
)
+ self._distinct_on_factory = distinct_on_factory or DistinctOnEnumFactory(
+ self._mapper, handle_cycles=handle_cycles, type_map=type_map
+ )
def _aggregation_field(
self, field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]], dto_config: DTOConfig
@@ -104,40 +111,121 @@ def _aggregation_field(
related_dto=dto,
)
- def _update_fields(
+ def _order_by_input_for_field(self, field: GraphQLFieldDefinition) -> type[OrderByDTO] | None:
+ if field.related_model is None:
+ return None
+ try:
+ order_by_input = self._order_by_factory.factory(
+ field.related_model, DTOConfig(Purpose.READ, partial=True, include="all"), if_no_fields="raise"
+ )
+ except EmptyDTOError:
+ order_by_input = None
+ return order_by_input
+
+ def _distinct_on_input_for_field(self, field: GraphQLFieldDefinition) -> type[EnumDTO] | None:
+ if field.related_model is None:
+ return None
+ try:
+ distinct_on_input = self._distinct_on_factory.factory(
+ field.related_model, DTOConfig(Purpose.READ, partial=True, include="all"), if_no_fields="raise"
+ )
+ except EmptyDTOError:
+ distinct_on_input = None
+ return distinct_on_input
+
+ def _json_field(self) -> StrawberryField:
+ return self._mapper.field(
+ root_field=False,
+ arguments=[
+ StrawberryArgument(JSON_PATH_KEY, None, type_annotation=StrawberryAnnotation(annotation=Optional[str]))
+ ],
+ )
+
+ def _add_fields_arguments(
self,
dto: type[GraphQLDTOT],
base: type[Any] | None,
- pagination: bool | DefaultOffsetPagination = False,
- order_by: bool = False,
+ order: IncludeFields | None = None,
+ paginate: IncludeFields | None = None,
+ distinct_on: IncludeFields | None = None,
+ default_pagination: None | DefaultOffsetPagination = None,
) -> type[GraphQLDTOT]:
+ """Add pagination and ordering arguments to a GraphQL DTO type.
+
+ Enhances a GraphQL Data Transfer Object (DTO) type with pagination and ordering
+ arguments for relation fields and path filtering for JSON fields. This is a
+ post-processing step that modifies the DTO type after initial generation to
+ add query capabilities.
+
+ For each relation field with `uselist=True` (one-to-many relationships):
+ - If included in the `order` specification: adds an order_by argument
+ - If included in the `paginate` specification: adds pagination configuration
+
+ For each JSON field:
+ - Adds a `json_path` argument for path-based filtering
+
+ Args:
+ dto: The GraphQL DTO type to enhance. Must be a generated strawberry type.
+ base: Optional base class whose annotations should be merged into the DTO.
+ If provided, annotations from the base class are added to the final DTO.
+ order: Field inclusion specification for ordering arguments. Can be:
+ - None: No ordering arguments added (default)
+ - "all": Add order_by arguments to all relation fields
+ - list/set of field names: Add order_by arguments only to named relations
+ distinct_on: Field inclusion specification for distinct_on arguments. Can be:
+ - None: No distinct_on arguments added (default)
+ - "all": Add distinct_on arguments to all relation fields
+ - list/set of field names: Add distinct_on arguments only to named relations
+ paginate: Field inclusion specification for pagination arguments. Can be:
+ - None: No pagination arguments added (default)
+ - "all": Add pagination to all relation fields
+ - list/set of field names: Add pagination only to named relations
+ default_pagination: Default pagination configuration to apply when
+ paginate is enabled. If None, uses default pagination (True).
+
+ Returns:
+ The modified DTO type with updated __annotations__ and attributes
+ containing the new pagination and ordering arguments.
+ """
attributes: dict[str, Any] = {}
annotations: dict[str, Any] = {}
+ order_config = DTOConfig.from_include(order)
+ pagination_config = DTOConfig.from_include(paginate)
+ distinct_on_config = DTOConfig.from_include(distinct_on)
- for field in dto.__strawchemy_field_map__.values():
+ for field in dto.__strawchemy_definition__.field_map.values():
+ # Add pagination, distinct_on and ordering arguments for relations
if field.is_relation and field.uselist:
related = Self if field.related_dto is dto else field.related_dto
type_annotation = list[related] if related is not None else field.type_
assert field.related_model
- order_by_input = None
- if order_by:
- order_by_input = self._order_by_factory.factory(field.related_model, read_all_partial_config)
- strawberry_field = self._mapper.field(pagination=pagination, order_by=order_by_input, root_field=False)
+ field_name = field.model_field_name
+ order_by_input, distinct_on_input, pagination = None, None, False
+ if order_config.is_field_included(field_name) or self._mapper.config.order_config.is_field_included(
+ field_name
+ ):
+ order_by_input = self._order_by_input_for_field(field)
+ if pagination_config.is_field_included(
+ field_name
+ ) or self._mapper.config.pagination_config.is_field_included(field_name):
+ pagination = default_pagination or True
+ if distinct_on_config.is_field_included(
+ field_name
+ ) or self._mapper.config.distinct_on_config.is_field_included(field_name):
+ distinct_on_input = self._distinct_on_input_for_field(field)
+
+ strawberry_field = self._mapper.field(
+ pagination=pagination, order_by=order_by_input, distinct_on=distinct_on_input, root_field=False
+ )
attributes[field.name] = strawberry_field
annotations[field.name] = type_annotation
+ # Add path filtering argument for JSON fields
elif (
not field.is_relation
and field.has_model_field
and self.inspector.model_field_type(field) in {JSON, dict}
):
- attributes[field.name] = self._mapper.field(
- root_field=False,
- arguments=[
- StrawberryArgument(
- JSON_PATH_KEY, None, type_annotation=StrawberryAnnotation(annotation=Optional[str])
- )
- ],
- )
+ attributes[field.name] = self._json_field()
annotations[field.name] = Union[field.type_, None]
dto.__annotations__ |= annotations
@@ -152,18 +240,6 @@ def _update_fields(
setattr(dto, name, value)
return dto
- @override
- def _cache_key(
- self,
- model: type[Any],
- dto_config: DTOConfig,
- node: Node[Relation[Any, MappedGraphQLDTOT], None],
- *,
- child_options: ChildOptions,
- **factory_kwargs: Any,
- ) -> Hashable:
- return (super()._cache_key(model, dto_config, node, **factory_kwargs), child_options)
-
@override
def dto_name(
self, base_name: str, dto_config: DTOConfig, node: Node[Relation[Any, MappedGraphQLDTOT], None] | None = None
@@ -178,7 +254,7 @@ def iter_field_definitions(
dto_config: DTOConfig,
base: type[DTOBase[DeclarativeBase]] | None,
node: Node[Relation[DeclarativeBase, MappedGraphQLDTOT], None],
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
*,
aggregations: bool = False,
field_map: dict[DTOKey, GraphQLFieldDefinition] | None = None,
@@ -186,7 +262,7 @@ def iter_field_definitions(
) -> Generator[DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]]]:
field_map = field_map if field_map is not None else {}
for field in super().iter_field_definitions(
- name, model, dto_config, base, node, raise_if_no_fields, field_map=field_map, **kwargs
+ name, model, dto_config, base, node, if_no_fields, field_map=field_map, **kwargs
):
key = DTOKey.from_dto_node(node)
if field.is_relation and field.uselist and aggregations:
@@ -202,68 +278,66 @@ def factory(
dto_config: DTOConfig,
base: type[Any] | None = None,
name: str | None = None,
- parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- current_node: Node[Relation[Any, MappedGraphQLDTOT], None] | None = None,
- raise_if_no_fields: bool = False,
- tags: set[str] | None = None,
- backend_kwargs: dict[str, Any] | None = None,
- *,
- child_options: ChildOptions | None = None,
- aggregations: bool = True,
- description: str | None = None,
- directives: Sequence[object] | None = (),
- override: bool = False,
- user_defined: bool = False,
- register_type: bool = True,
- **kwargs: Any,
+ **kwargs: Unpack[FactoryMethodKwargs],
) -> type[MappedGraphQLDTOT]:
- dto = super().factory(
- model,
- dto_config,
- base,
- name,
- parent_field_def,
- current_node,
- raise_if_no_fields,
- tags,
- backend_kwargs,
- aggregations=aggregations if dto_config.purpose is Purpose.READ else False,
- register_type=False,
- override=override,
- child_options=child_options,
- **kwargs,
- )
- child_options = child_options or ChildOptions()
+ aggregations = kwargs.get("aggregations", True)
+ paginate = kwargs.get("paginate")
+ order = kwargs.get("order")
+ distinct_on = kwargs.get("distinct_on")
+ default_pagination = kwargs.get("default_pagination")
+ description = kwargs.get("description")
+ directives = kwargs.get("directives", ())
+ override_ = kwargs.get("override", False)
+ user_defined = kwargs.get("user_defined", False)
+ register_type = kwargs.get("register_type", True)
+ kwargs["register_type"] = False
+ kwargs["aggregations"] = aggregations if dto_config.purpose is Purpose.READ else False
+ kwargs["paginate"] = paginate if paginate == "all" else self._mapper.config.pagination
+ kwargs["order"] = order if order == "all" else self._mapper.config.order_by
+ kwargs["distinct_on"] = distinct_on if distinct_on == "all" else self._mapper.config.distinct_on
+ dto = super().factory(model, dto_config, base, name, **kwargs)
if self.graphql_type(dto_config) == "object":
- dto = self._update_fields(dto, base, pagination=child_options.pagination, order_by=child_options.order_by)
+ dto = self._add_fields_arguments(
+ dto,
+ base,
+ order=order if is_fields_iterable(order) else None,
+ distinct_on=distinct_on,
+ paginate=paginate,
+ default_pagination=default_pagination,
+ )
if register_type:
- return self._register_type(
+ return self._mapper.registry.register_type(
dto,
+ graphql_type=self.graphql_type(dto_config),
dto_config=dto_config,
description=description,
directives=directives,
- override=override,
+ override=override_,
user_defined=user_defined,
- child_options=child_options,
- current_node=current_node,
+ default_pagination=default_pagination,
+ order=order,
+ distinct_on=distinct_on,
+ paginate=paginate,
+ current_node=kwargs.get("current_node"),
+ default_name=self.root_dto_name(model, dto_config),
)
return dto
-class RootAggregateTypeDTOFactory(TypeDTOFactory[MappedGraphQLDTOT]):
+class AggregateRootTypeFactory(ObjectTypeFactory[MappedGraphQLDTOT]):
def __init__(
self,
mapper: Strawchemy,
backend: DTOBackend[MappedGraphQLDTOT],
handle_cycles: bool = True,
type_map: dict[Any, Any] | None = None,
- type_factory: TypeDTOFactory[MappedGraphQLDTOT] | None = None,
- aggregation_factory: AggregateDTOFactory[AggregateDTOT] | None = None,
+ type_factory: ObjectTypeFactory[MappedGraphQLDTOT] | None = None,
+ aggregation_factory: AggregateFieldsFactory[AggregateDTOT] | None = None,
**kwargs: Any,
) -> None:
super().__init__(mapper, backend, handle_cycles, type_map, **kwargs)
- self._type_factory = type_factory or TypeDTOFactory(mapper, backend)
- self._aggregation_factory = aggregation_factory or AggregateDTOFactory(
+ self._type_factory = type_factory or ObjectTypeFactory(mapper, backend)
+ self._aggregation_factory = aggregation_factory or AggregateFieldsFactory(
mapper, StrawberrryDTOBackend(AggregateDTO)
)
@@ -281,7 +355,7 @@ def iter_field_definitions(
dto_config: DTOConfig,
base: type[DTOBase[DeclarativeBase]] | None,
node: Node[Relation[DeclarativeBase, MappedGraphQLDTOT], None],
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
aggregations: bool = False,
field_map: dict[DTOKey, GraphQLFieldDefinition] | None = None,
**kwargs: Any,
@@ -317,33 +391,14 @@ def factory(
dto_config: DTOConfig,
base: type[Any] | None = None,
name: str | None = None,
- parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- current_node: Node[Relation[Any, MappedGraphQLDTOT], None] | None = None,
- raise_if_no_fields: bool = False,
- tags: set[str] | None = None,
- backend_kwargs: dict[str, Any] | None = None,
- *,
- aggregations: bool = True,
- **kwargs: Any,
+ **kwargs: Unpack[FactoryMethodKwargs],
) -> type[MappedGraphQLDTOT]:
- dto = super().factory(
- model,
- dto_config,
- base,
- name,
- parent_field_def,
- current_node,
- raise_if_no_fields,
- tags,
- backend_kwargs,
- aggregations=aggregations,
- **kwargs,
- )
- dto.__strawchemy_is_root_aggregation_type__ = True
+ dto: type[MappedGraphQLDTOT] = super().factory(model, dto_config, base, name, **kwargs)
+ dto.__strawchemy_definition__.is_root_aggregation_type = True
return dto
-class AggregateDTOFactory(GraphQLDTOFactory[AggregateDTOT]):
+class AggregateFieldsFactory(GraphQLFactory[AggregateDTOT]):
def __init__(
self,
mapper: Strawchemy,
@@ -375,7 +430,7 @@ def _factory(
node: Node[Relation[Any, AggregateDTOT], None],
base: type[Any] | None = None,
parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
backend_kwargs: dict[str, Any] | None = None,
field_map: dict[DTOKey, GraphQLFieldDefinition] | None = None,
**kwargs: Any,
@@ -401,25 +456,35 @@ def _factory(
return self.backend.build(name, model, field_definitions, **(backend_kwargs or {}))
-class DistinctOnFieldsDTOFactory(EnumDTOFactory):
+class DistinctOnEnumFactory(EnumFactory):
@override
def dto_name(
self, base_name: str, dto_config: DTOConfig, node: Node[Relation[Any, EnumDTO], None] | None = None
) -> str:
return f"{base_name}DistinctOnFields"
+ @override
+ def _resolve_relation_type(
+ self,
+ field: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]],
+ dto_config: DTOConfig,
+ node: Node[Relation[Any, EnumDTO], None],
+ **factory_kwargs: Any,
+ ) -> Any:
+ return super()._resolve_relation_type(field, dto_config.copy_with(include="all"), node, **factory_kwargs)
+
-class UpsertConflictFieldsDTOFactory(EnumDTOFactory):
+class UpsertConflictEnumFactory(EnumFactory):
inspector: SQLAlchemyGraphQLInspector
def __init__(
self,
- inspector: SQLAlchemyGraphQLInspector,
- backend: UpsertConflictFieldsEnumDTOBackend | None = None,
+ mapper: Strawchemy,
+ backend: UpsertConflictEnumBackend | None = None,
handle_cycles: bool = True,
type_map: dict[Any, Any] | None = None,
) -> None:
- super().__init__(inspector, backend or UpsertConflictFieldsEnumDTOBackend(inspector), handle_cycles, type_map)
+ super().__init__(mapper, backend or UpsertConflictEnumBackend(mapper.config.inspector), handle_cycles, type_map)
@override
def dto_name(
@@ -435,7 +500,7 @@ def iter_field_definitions(
dto_config: DTOConfig,
base: type[DTOBase[DeclarativeBase]] | None,
node: Node[Relation[DeclarativeBase, EnumDTO], None],
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
**kwargs: Any,
) -> Generator[DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]]]:
constraints = self.inspector.unique_constraints(model)
@@ -474,7 +539,7 @@ def should_exclude_field(
)
-class InputFactory(TypeDTOFactory[MappedGraphQLDTOT]):
+class MutationInputFactory(ObjectTypeFactory[MappedGraphQLDTOT]):
def __init__(
self,
mapper: Strawchemy,
@@ -486,8 +551,8 @@ def __init__(
super().__init__(mapper, backend, handle_cycles, type_map, **kwargs)
self._identifier_input_dto_builder = StrawberrryDTOBackend(MappedStrawberryGraphQLDTO[DeclarativeBase])
self._identifier_input_dto_factory = DTOFactory(self.inspector, self.backend)
- self._upsert_update_fields_enum_factory = EnumDTOFactory(self.inspector)
- self._upsert_conflict_fields_enum_factory = UpsertConflictFieldsDTOFactory(self.inspector)
+ self._upsert_update_fields_enum_factory = EnumFactory(self._mapper)
+ self._upsert_conflict_fields_enum_factory = UpsertConflictEnumFactory(self._mapper)
def _identifier_input(
self,
@@ -503,7 +568,7 @@ def _identifier_input(
if base is None:
try:
base = self._identifier_input_dto_factory.factory(
- related_model, dto_config, name=name, raise_if_no_fields=True
+ related_model, dto_config, name=name, if_no_fields="raise"
)
except EmptyDTOError as error:
msg = (
@@ -512,7 +577,9 @@ def _identifier_input(
)
raise EmptyDTOError(msg) from error
- return self._register_type(base, dto_config, node, description="Identifier input", user_defined=False)
+ return self._mapper.registry.register_type(
+ base, graphql_type="input", dto_config=dto_config, description="Identifier input", user_defined=False
+ )
def _upsert_udpate_fields(
self,
@@ -523,10 +590,12 @@ def _upsert_udpate_fields(
name = f"{node.root.value.model.__name__}{snake_to_camel(field.name)}UpdateFields"
related_model = field.related_model
assert related_model
- update_fields = self._upsert_update_fields_enum_factory.factory(
- related_model, dto_config.copy_with(purpose=Purpose.WRITE, include="all"), name=name
+ return self._upsert_update_fields_enum_factory.factory(
+ related_model,
+ dto_config.copy_with(purpose=Purpose.WRITE, include="all"),
+ name=name,
+ description="Update fields enum",
)
- return self._mapper.registry.register_enum(update_fields, name=name, description="Update fields enum")
def _upsert_conflict_fields(
self,
@@ -537,10 +606,12 @@ def _upsert_conflict_fields(
name = f"{node.root.value.model.__name__}{snake_to_camel(field.name)}ConflictFields"
related_model = field.related_model
assert related_model
- conflict_fields = self._upsert_conflict_fields_enum_factory.factory(
- related_model, dto_config.copy_with(purpose=Purpose.WRITE, include="all"), name=name
+ return self._upsert_conflict_fields_enum_factory.factory(
+ related_model,
+ dto_config.copy_with(purpose=Purpose.WRITE, include="all"),
+ name=name,
+ description="Conflict fields enum",
)
- return self._mapper.registry.register_enum(conflict_fields, name=name, description="Conflict fields enum")
def _description(self, mode: GraphQLPurpose) -> str:
if mode == "create_input":
@@ -558,12 +629,11 @@ def _cache_key(
dto_config: DTOConfig,
node: Node[Relation[Any, MappedGraphQLDTOT], None],
*,
- child_options: ChildOptions,
mode: GraphQLPurpose,
**factory_kwargs: Any,
) -> Hashable:
return (
- super()._cache_key(model, dto_config, node, child_options=child_options, **factory_kwargs),
+ super()._cache_key(model, dto_config, node, **factory_kwargs),
node.root.value.model,
mode,
)
@@ -642,13 +712,13 @@ def iter_field_definitions(
dto_config: DTOConfig,
base: type[DTOBase[DeclarativeBase]] | None,
node: Node[Relation[DeclarativeBase, MappedGraphQLDTOT], None],
- raise_if_no_fields: bool = False,
+ if_no_fields: Literal["raise", "skip"] = "skip",
*,
mode: GraphQLPurpose,
**factory_kwargs: Any,
) -> Generator[DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]]]:
for field in super().iter_field_definitions(
- name, model, dto_config, base, node, raise_if_no_fields, mode=mode, **factory_kwargs
+ name, model, dto_config, base, node, if_no_fields, mode=mode, **factory_kwargs
):
if mode == "update_by_pk_input" and self.inspector.is_primary_key(field.model_field):
field.type_ = non_optional_type_hint(field.type_)
@@ -661,27 +731,10 @@ def factory(
dto_config: DTOConfig = read_partial,
base: type[Any] | None = None,
name: str | None = None,
- parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- current_node: Node[Relation[Any, MappedGraphQLDTOT], None] | None = None,
- raise_if_no_fields: bool = False,
- tags: set[str] | None = None,
- backend_kwargs: dict[str, Any] | None = None,
- *,
- description: str | None = None,
- mode: GraphQLPurpose,
- **kwargs: Any,
+ **kwargs: Unpack[FactoryMethodKwargs],
) -> type[MappedGraphQLDTOT]:
- return super().factory(
- model,
- dto_config,
- base,
- name,
- parent_field_def,
- current_node,
- raise_if_no_fields,
- tags=tags or set() | {mode},
- backend_kwargs=backend_kwargs,
- description=description or self._description(mode),
- mode=mode,
- **kwargs,
- )
+ mode = kwargs.get("mode")
+ assert mode is not None, "InputFactory.factory requires `mode`"
+ kwargs["tags"] = kwargs.get("tags") or (set() | {mode})
+ kwargs["description"] = kwargs.get("description") or self._description(mode)
+ return super().factory(model, dto_config, base, name, **kwargs)
diff --git a/src/strawchemy/schema/field.py b/src/strawchemy/schema/field.py
index 32a21f04..3583f060 100644
--- a/src/strawchemy/schema/field.py
+++ b/src/strawchemy/schema/field.py
@@ -14,9 +14,15 @@
from strawchemy.constants import DISTINCT_ON_KEY, FILTER_KEY, LIMIT_KEY, NODES_KEY, OFFSET_KEY, ORDER_BY_KEY
from strawchemy.dto.base import MappedDTO
-from strawchemy.dto.strawberry import MappedStrawberryGraphQLDTO, StrawchemyDTOAttributes
-from strawchemy.dto.types import DTOConfig, Purpose
-from strawchemy.exceptions import StrawchemyFieldError
+from strawchemy.dto.strawberry import (
+ BooleanFilterDTO,
+ EnumDTO,
+ MappedStrawberryGraphQLDTO,
+ OrderByDTO,
+ StrawchemyObject,
+)
+from strawchemy.dto.types import DTOConfig, IncludeFields, Purpose
+from strawchemy.exceptions import EmptyDTOError, StrawchemyFieldError
from strawchemy.schema.pagination import DefaultOffsetPagination
from strawchemy.utils.annotation import is_type_hint_optional
from strawchemy.utils.strawberry import (
@@ -37,10 +43,11 @@
from strawberry.types.fields.resolver import StrawberryResolver
from strawchemy import StrawchemyConfig
- from strawchemy.dto.strawberry import BooleanFilterDTO, EnumDTO, OrderByDTO
from strawchemy.repository.strawberry import StrawchemyAsyncRepository, StrawchemySyncRepository
from strawchemy.repository.strawberry.base import GraphQLResult
from strawchemy.repository.typing import QueryHookCallable
+ from strawchemy.schema.factories import DistinctOnEnumFactory
+ from strawchemy.schema.factories.inputs import BooleanFilterFactory, OrderByFactory
from strawchemy.typing import (
AnyRepository,
AnyRepositoryType,
@@ -48,7 +55,7 @@
FilterStatementCallable,
GetByIdResolverResult,
ListResolverResult,
- StrawchemyTypeWithStrawberryObjectDefinition,
+ StrawchemyObjectWithStrawberryObjectDefinition,
)
__all__ = ("StrawchemyField",)
@@ -74,17 +81,20 @@ class StrawchemyField(StrawberryField):
def __init__(
self,
config: StrawchemyConfig,
- repository_type: AnyRepositoryType,
- filter_type: type[BooleanFilterDTO] | None = None,
- order_by: type[OrderByDTO] | None = None,
- distinct_on: type[EnumDTO] | None = None,
- pagination: bool | DefaultOffsetPagination = False,
+ order_by_factory: OrderByFactory,
+ filter_factory: BooleanFilterFactory,
+ distinct_on_factory: DistinctOnEnumFactory,
+ filter_type: type[BooleanFilterDTO] | bool | None = None,
+ order_by: IncludeFields | type[OrderByDTO] | Literal[False] | None = None,
+ distinct_on: IncludeFields | type[EnumDTO] | Literal[False] | None = None,
+ pagination: DefaultOffsetPagination | bool | None = False,
+ repository_type: AnyRepositoryType | None = None,
root_aggregations: bool = False,
registry_namespace: dict[str, Any] | None = None,
filter_statement: FilterStatementCallable | None = None,
query_hook: QueryHookCallable[Any] | Sequence[QueryHookCallable[Any]] | None = None,
execution_options: dict[str, Any] | None = None,
- id_field_name: str = "id",
+ id_field_name: str | None = None,
arguments: list[StrawberryArgument] | None = None,
# Original StrawberryField args
python_name: str | None = None,
@@ -107,20 +117,22 @@ def __init__(
self.registry_namespace = registry_namespace
self.is_root_field = root_field
self.root_aggregations = root_aggregations
- self.distinct_on = distinct_on
self.query_hook = query_hook
- self.pagination: DefaultOffsetPagination | Literal[False] = (
- DefaultOffsetPagination() if pagination is True else pagination
- )
- self.id_field_name = id_field_name
+ self.id_field_name = config.default_id_field_name if id_field_name is None else id_field_name
+
+ self._pagination = pagination
+ self._distinct_on = distinct_on
self._filter = filter_type
self._order_by = order_by
self._description = description
self._filter_statement = filter_statement
- self._execution_options = execution_options
+ self._execution_options = config.execution_options if execution_options is None else execution_options
self._config = config
- self._repository_type = repository_type
+ self._repository_type = config.repository_type if repository_type is None else repository_type
+ self._order_by_factory = order_by_factory
+ self._filter_factory = filter_factory
+ self._distinct_on_factory = distinct_on_factory
super().__init__(
python_name,
@@ -150,8 +162,8 @@ def _type_or_annotation(
return type_
@property
- def _strawchemy_type(self) -> type[StrawchemyTypeWithStrawberryObjectDefinition]:
- return cast("type[StrawchemyTypeWithStrawberryObjectDefinition]", self.type)
+ def _strawchemy_type(self) -> type[StrawchemyObjectWithStrawberryObjectDefinition]:
+ return cast("type[StrawchemyObjectWithStrawberryObjectDefinition]", self.type)
def _get_repository(self, info: Info[Any, Any]) -> StrawchemySyncRepository[Any] | StrawchemyAsyncRepository[Any]:
return self._repository_type(
@@ -209,8 +221,8 @@ def _validate_type(self, type_: StrawberryType | type[WithStrawberryObjectDefini
for inner_type in strawberry_contained_types(type_):
if (
self.root_aggregations
- and issubclass(inner_type, StrawchemyDTOAttributes)
- and not inner_type.__strawchemy_is_root_aggregation_type__
+ and issubclass(inner_type, StrawchemyObject)
+ and not inner_type.__strawchemy_definition__.is_root_aggregation_type
):
msg = f"The `{self.name}` field is defined with `root_aggregations` enabled but the field type is not a root aggregation type."
raise StrawchemyFieldError(msg)
@@ -223,19 +235,99 @@ def _is_strawchemy_type(
isclass(type_) and issubclass(type_, MappedStrawberryGraphQLDTO)
)
- @cached_property
- def filter(self) -> type[BooleanFilterDTO] | None:
+ @property
+ def pagination(self) -> DefaultOffsetPagination | None:
+ if self._pagination is True or (
+ self._pagination is None and self._config.pagination_config.is_field_included(self.python_name)
+ ):
+ return DefaultOffsetPagination(
+ limit=self._config.pagination_default_limit, offset=self._config.pagination_default_offset
+ )
+
+ return None if not self._pagination else self._pagination
+
+ @property
+ def distinct_on(self) -> type[EnumDTO] | None:
+ if isclass(self._distinct_on) and issubclass(self._distinct_on, EnumDTO): # pyright: ignore[reportUnnecessaryIsInstance]
+ return self._distinct_on
+
inner_type = strawberry_contained_user_type(self.type)
- if self._filter is None and self._is_strawchemy_type(inner_type):
- return inner_type.__strawchemy_filter__
- return self._filter
+ distinct_on = self._config.distinct_on if self._distinct_on is None else self._distinct_on
+
+ if self._is_strawchemy_type(inner_type) and distinct_on:
+ try:
+ return self._distinct_on_factory.factory(
+ inner_type.__dto_model__, # type: ignore[reportGeneralTypeIssues]
+ dto_config=inner_type.__dto_config__.copy_with(
+ include=inner_type.__dto_config__.include if distinct_on == "all" else distinct_on
+ ),
+ no_cache=True,
+ if_no_fields="raise",
+ )
+ except EmptyDTOError:
+ return None
- @cached_property
+ if (
+ self._distinct_on is None
+ and self._is_strawchemy_type(inner_type)
+ and inner_type.__strawchemy_definition__.distinct_on is not None
+ ):
+ return inner_type.__strawchemy_definition__.distinct_on
+
+ return None
+
+ @property
def order_by(self) -> type[OrderByDTO] | None:
+ if isclass(self._order_by) and issubclass(self._order_by, OrderByDTO): # pyright: ignore[reportUnnecessaryIsInstance]
+ return self._order_by
+
+ inner_type = strawberry_contained_user_type(self.type)
+ order_by = self._config.order_by if self._order_by is None else self._order_by
+
+ if self._is_strawchemy_type(inner_type) and order_by:
+ try:
+ return self._order_by_factory.make_input(
+ inner_type.__dto_model__, # type: ignore[reportGeneralTypeIssues]
+ mode="order_by",
+ dto_config=inner_type.__dto_config__.copy_with(
+ include=inner_type.__dto_config__.include if order_by == "all" else order_by
+ ),
+ no_cache=True,
+ if_no_fields="raise",
+ )
+ except EmptyDTOError:
+ return None
+ if (
+ self._order_by is None
+ and self._is_strawchemy_type(inner_type)
+ and inner_type.__strawchemy_definition__.order_by is not None
+ ):
+ return inner_type.__strawchemy_definition__.order_by
+
+ return None
+
+ @cached_property
+ def filter(self) -> type[BooleanFilterDTO] | None:
+ if isclass(self._filter) and issubclass(self._filter, BooleanFilterDTO): # pyright: ignore[reportUnnecessaryIsInstance]
+ return self._filter
+
inner_type = strawberry_contained_user_type(self.type)
- if self._order_by is None and self._is_strawchemy_type(inner_type):
- return inner_type.__strawchemy_order_by__
- return self._order_by
+
+ if self._is_strawchemy_type(inner_type) and self._filter is True:
+ return self._filter_factory.make_input(
+ inner_type.__dto_model__, # type: ignore[reportGeneralTypeIssues]
+ mode="filter",
+ dto_config=inner_type.__dto_config__,
+ no_cache=True,
+ )
+ if (
+ self._filter is None
+ and self._is_strawchemy_type(inner_type)
+ and inner_type.__strawchemy_definition__.filter is not None
+ ):
+ return inner_type.__strawchemy_definition__.filter
+
+ return None
def auto_arguments(self) -> list[StrawberryArgument]:
arguments: list[StrawberryArgument] = []
@@ -349,11 +441,14 @@ def __copy__(self) -> Self:
root_aggregations=self.root_aggregations,
filter_type=self._filter,
order_by=self._order_by,
- distinct_on=self.distinct_on,
+ distinct_on=self._distinct_on,
pagination=self.pagination,
registry_namespace=self.registry_namespace,
execution_options=self._execution_options,
config=self._config,
+ order_by_factory=self._order_by_factory,
+ filter_factory=self._filter_factory,
+ distinct_on_factory=self._distinct_on_factory,
)
new_field._arguments = self._arguments[:] if self._arguments is not None else None # noqa: SLF001
return new_field
diff --git a/src/strawchemy/schema/mutation/field_builder.py b/src/strawchemy/schema/mutation/field_builder.py
index 72903bb2..8734cd6a 100644
--- a/src/strawchemy/schema/mutation/field_builder.py
+++ b/src/strawchemy/schema/mutation/field_builder.py
@@ -15,6 +15,8 @@
from strawberry.extensions.field_extension import FieldExtension
from strawchemy.config.base import StrawchemyConfig
+ from strawchemy.schema.factories import DistinctOnEnumFactory
+ from strawchemy.schema.factories.inputs import BooleanFilterFactory, OrderByFactory
from strawchemy.schema.mutation.fields import (
StrawchemyCreateMutationField,
StrawchemyDeleteMutationField,
@@ -35,6 +37,9 @@ class MutationFieldBuilder:
config: StrawchemyConfig
registry_namespace_getter: Callable[[], dict[str, Any]]
+ order_by_factory: OrderByFactory
+ filter_factory: BooleanFilterFactory
+ distinct_on_factory: DistinctOnEnumFactory
def build(
self,
@@ -86,11 +91,10 @@ def build(
"""
namespace = self.registry_namespace_getter()
type_annotation = StrawberryAnnotation.from_annotation(graphql_type, namespace) if graphql_type else None
- repository_type_ = repository_type if repository_type is not None else self.config.repository_type
field = field_class(
config=self.config,
- repository_type=repository_type_,
+ repository_type=repository_type,
python_name=None,
graphql_name=name,
type_annotation=type_annotation,
@@ -104,6 +108,9 @@ def build(
extensions=extensions or [],
registry_namespace=namespace,
description=description,
+ order_by_factory=self.order_by_factory,
+ filter_factory=self.filter_factory,
+ distinct_on_factory=self.distinct_on_factory,
**field_specific_kwargs,
)
return field(resolver) if resolver else field
diff --git a/src/strawchemy/schema/mutation/input.py b/src/strawchemy/schema/mutation/input.py
index 2c82653a..1a9f4a8c 100644
--- a/src/strawchemy/schema/mutation/input.py
+++ b/src/strawchemy/schema/mutation/input.py
@@ -261,7 +261,8 @@ def __init__(
for index, dto in enumerate(dtos):
mapped = dto.to_mapped(
visitor=_InputVisitor(
- self, is_update=dto.__strawchemy_purpose__ in ("update_by_pk_input", "update_by_filter_input")
+ self,
+ is_update=dto.__strawchemy_definition__.is_update_purpose,
),
override=override,
)
diff --git a/src/strawchemy/typing.py b/src/strawchemy/typing.py
index c0984dc9..7391f396 100644
--- a/src/strawchemy/typing.py
+++ b/src/strawchemy/typing.py
@@ -20,7 +20,7 @@
OrderByDTO,
OutputFunctionInfo,
QueryNodeMetadata,
- StrawchemyDTOAttributes,
+ StrawchemyObject,
UnmappedStrawberryGraphQLDTO,
)
from strawchemy.utils.graph import Node
@@ -51,7 +51,7 @@
"QueryNodeType",
"QueryObject",
"StrawberryGraphQLDTO",
- "StrawchemyTypeWithStrawberryObjectDefinition",
+ "StrawchemyObjectWithStrawberryObjectDefinition",
"SupportedDialect",
)
@@ -97,10 +97,10 @@
AnyMappedDTO: TypeAlias = "MappedStrawberryGraphQLDTO[Any] | MappedPydanticGraphQLDTO[Any]"
QueryNodeType: TypeAlias = "Node[GraphQLFieldDefinition, QueryNodeMetadata]"
OneOrManyResult: TypeAlias = (
- "Sequence[StrawchemyTypeWithStrawberryObjectDefinition] | StrawchemyTypeWithStrawberryObjectDefinition"
+ "Sequence[StrawchemyObjectWithStrawberryObjectDefinition] | StrawchemyObjectWithStrawberryObjectDefinition"
)
ListResolverResult: TypeAlias = OneOrManyResult
-GetByIdResolverResult: TypeAlias = "StrawchemyTypeWithStrawberryObjectDefinition | None"
+GetByIdResolverResult: TypeAlias = "StrawchemyObjectWithStrawberryObjectDefinition | None"
CreateOrUpdateResolverResult: TypeAlias = "OneOrManyResult | ValidationErrorType | Sequence[ValidationErrorType]"
@@ -109,4 +109,4 @@
class DataclassProtocol(Protocol):
__dataclass_fields__: ClassVar[dict[str, Any]]
- class StrawchemyTypeWithStrawberryObjectDefinition(StrawchemyDTOAttributes, WithStrawberryObjectDefinition): ...
+ class StrawchemyObjectWithStrawberryObjectDefinition(StrawchemyObject, WithStrawberryObjectDefinition): ...
diff --git a/src/strawchemy/utils/registry.py b/src/strawchemy/utils/registry.py
index ad060ac8..84072047 100644
--- a/src/strawchemy/utils/registry.py
+++ b/src/strawchemy/utils/registry.py
@@ -12,6 +12,9 @@
from strawberry.types.base import StrawberryContainer
from strawberry.types.field import StrawberryField
+from strawchemy.dto.strawberry import MappedStrawberryGraphQLDTO
+from strawchemy.dto.types import cast_include_fields, is_fields_iterable
+from strawchemy.exceptions import StrawchemyError
from strawchemy.utils.strawberry import strawberry_contained_types
try:
@@ -22,7 +25,7 @@
geo_comparison = None
if TYPE_CHECKING:
- from collections.abc import Hashable, Iterable, Sequence
+ from collections.abc import Hashable, Sequence
from sqlalchemy.orm import DeclarativeBase
from strawberry.experimental.pydantic.conversion_types import PydanticModel, StrawberryTypeFromPydantic
@@ -30,14 +33,18 @@
from strawberry.types.arguments import StrawberryArgument
from strawberry.types.base import WithStrawberryObjectDefinition
- from strawchemy.dto.types import DTOScope
+ from strawchemy.dto import DTOConfig
+ from strawchemy.dto.base import Node, Relation
+ from strawchemy.dto.strawberry import EnumDTO, OrderByDTO, StrawchemyObject
+ from strawchemy.dto.types import DTOScope, IncludeFields
from strawchemy.schema.pagination import DefaultOffsetPagination
- from strawchemy.typing import GraphQLType, StrawchemyTypeWithStrawberryObjectDefinition
+ from strawchemy.typing import GraphQLType, StrawchemyObjectWithStrawberryObjectDefinition
__all__ = ("RegistryTypeInfo", "StrawberryRegistry")
T = TypeVar("T")
EnumT = TypeVar("EnumT", bound=Enum)
+StrawchemyDTOT = TypeVar("StrawchemyDTOT", bound="StrawchemyObject")
_RegistryMissing = NewType("_RegistryMissing", object)
@@ -101,8 +108,10 @@ class RegistryTypeInfo:
default_name: str | None = None
user_defined: bool = False
override: bool = False
- pagination: DefaultOffsetPagination | Literal[False] = False
- order_by: bool = False
+ pagination: DefaultOffsetPagination | None = None
+ order: frozenset[str] | Literal["all"] | type[OrderByDTO] = dataclasses.field(default_factory=frozenset)
+ distinct_on: frozenset[str] | Literal["all"] | type[EnumDTO] = dataclasses.field(default_factory=frozenset)
+ paginate: frozenset[str] | Literal["all"] = dataclasses.field(default_factory=frozenset)
scope: DTOScope | None = None
model: type[DeclarativeBase] | None = None
tags: frozenset[str] = dataclasses.field(default_factory=frozenset)
@@ -110,23 +119,24 @@ class RegistryTypeInfo:
@property
def scoped_id(self) -> Hashable:
- return (self.model, self.graphql_type, self.tags)
+ return self.model, self.graphql_type, self.tags
class StrawberryRegistry:
def __init__(self, strawberry_config: StrawberryConfig) -> None:
self.strawberry_config = strawberry_config
- self._namespaces: defaultdict[GraphQLType, dict[str, type[StrawchemyTypeWithStrawberryObjectDefinition]]] = (
+ self._namespaces: defaultdict[GraphQLType, dict[str, type[StrawchemyObjectWithStrawberryObjectDefinition]]] = (
defaultdict(dict)
)
self._forward_type_refs: defaultdict[GraphQLType, defaultdict[str, list[_TypeReference]]] = defaultdict(
lambda: defaultdict(list)
)
self._type_refs: defaultdict[Hashable, list[_TypeReference]] = defaultdict(list)
- self._scoped_types: dict[Hashable, type[StrawchemyTypeWithStrawberryObjectDefinition]] = {}
+ self._scoped_types: dict[Hashable, type[StrawchemyObjectWithStrawberryObjectDefinition]] = {}
self._type_map: dict[RegistryTypeInfo, type[Any]] = {}
self._names_map: defaultdict[GraphQLType, dict[str, RegistryTypeInfo]] = defaultdict(dict)
self._tracked_type_names: defaultdict[GraphQLType, set[str]] = defaultdict(set)
+ self._unique_names: defaultdict[str, int] = defaultdict(int)
def _get_field_type_name(
self,
@@ -216,7 +226,7 @@ def _track_references(
self._update_references(argument, "input")
self._update_references(field, graphql_type)
- def _register_type(self, type_info: RegistryTypeInfo, strawberry_type: type[Any]) -> None:
+ def _register(self, type_info: RegistryTypeInfo, strawberry_type: type[Any]) -> None:
"""Register a type in the registry.
This will add the type to the namespace, update forward references, and track the references of the type.
@@ -229,7 +239,8 @@ def _register_type(self, type_info: RegistryTypeInfo, strawberry_type: type[Any]
if type_info.override or type_info.scope == "global":
for reference in self._forward_type_refs[type_info.graphql_type][type_info.name]:
reference.update_type(strawberry_type)
- self._track_references(strawberry_type, type_info.graphql_type, force=type_info.override)
+ if type_info.graphql_type != "enum":
+ self._track_references(strawberry_type, type_info.graphql_type, force=type_info.override)
if type_info.scope == "global" and type_info.model:
if type_info.default_name:
self._namespaces[type_info.graphql_type][type_info.default_name] = strawberry_type
@@ -286,16 +297,16 @@ def _check_conflicts(self, type_info: RegistryTypeInfo) -> None:
"""
if (
self.non_override_exists(type_info)
- or self.namespace("enum").get(type_info.name)
- or self.name_clash(type_info)
+ or (type_info.graphql_type != "enum" and self.namespace("enum").get(type_info.name))
+ or self._name_clash(type_info)
):
- msg = f"Type {type_info.name} is already registered"
- raise ValueError(msg)
+ msg = f"Type `{type_info.name}` is already registered"
+ raise StrawchemyError(msg)
def __contains__(self, type_info: RegistryTypeInfo) -> bool:
return type_info in self._type_map
- def name_clash(self, type_info: RegistryTypeInfo) -> bool:
+ def _name_clash(self, type_info: RegistryTypeInfo) -> bool:
return (
type_info not in self
and (existing := self.get(type_info.graphql_type, type_info.name, None)) is not None
@@ -303,6 +314,48 @@ def name_clash(self, type_info: RegistryTypeInfo) -> bool:
and not type_info.override
)
+ def _get_type_info(
+ self,
+ dto: type[StrawchemyObject | Enum],
+ graphql_type: GraphQLType,
+ dto_config: DTOConfig,
+ current_node: Node[Relation[Any, Any], None] | None,
+ override: bool = False,
+ user_defined: bool = False,
+ paginate: IncludeFields | None = None,
+ order: IncludeFields | type[OrderByDTO] | None = None,
+ distinct_on: IncludeFields | type[EnumDTO] | None = None,
+ default_pagination: DefaultOffsetPagination | None = None,
+ default_name: str | None = None,
+ ) -> RegistryTypeInfo:
+ model: type[DeclarativeBase] | None = dto.__dto_model__ if issubclass(dto, MappedStrawberryGraphQLDTO) else None # type: ignore[reportGeneralTypeIssues]
+ type_info = RegistryTypeInfo(
+ name=dto.__name__,
+ default_name=default_name,
+ graphql_type=graphql_type,
+ override=override,
+ user_defined=user_defined,
+ pagination=default_pagination,
+ order=cast_include_fields(order) if is_fields_iterable(order) else order,
+ distinct_on=cast_include_fields(distinct_on) if is_fields_iterable(distinct_on) else distinct_on,
+ paginate=cast_include_fields(paginate),
+ scope=dto_config.scope,
+ model=model,
+ exclude_from_scope=dto_config.exclude_from_scope,
+ )
+ if self._name_clash(type_info) and current_node is not None:
+ type_info = dataclasses.replace(
+ type_info, name="".join(node.value.name for node in current_node.path_from_root())
+ )
+ return type_info
+
+ def uniquify_name(self, graphql_type: GraphQLType, name: str) -> str:
+ """Return a type name guaranteed to be unique within the registry."""
+ while self.get(graphql_type, name, None):
+ self._unique_names[name] += 1
+ name = f"{name}{self._unique_names[name]}"
+ return name
+
@overload
def get(self, graphql_type: GraphQLType, name: str, default: _RegistryMissing) -> RegistryTypeInfo: ...
@@ -330,39 +383,75 @@ def namespace(self, graphql_type: GraphQLType) -> dict[str, type[Any]]:
def register_type(
self,
- type_: type[Any],
- type_info: RegistryTypeInfo,
+ dto: type[StrawchemyDTOT],
+ graphql_type: GraphQLType,
+ dto_config: DTOConfig,
+ current_node: Node[Relation[Any, Any], None] | None = None,
+ override: bool = False,
+ user_defined: bool = False,
+ paginate: IncludeFields | None = None,
+ order: IncludeFields | type[OrderByDTO] | None = None,
+ distinct_on: IncludeFields | type[EnumDTO] | None = None,
+ default_pagination: DefaultOffsetPagination | None = None,
+ default_name: str | None = None,
description: str | None = None,
directives: Sequence[object] | None = (),
- ) -> type[Any]:
+ ) -> type[StrawchemyDTOT]:
+ type_info = self._get_type_info(
+ dto=dto,
+ graphql_type=graphql_type,
+ dto_config=dto_config,
+ current_node=current_node,
+ override=override,
+ user_defined=user_defined,
+ paginate=paginate,
+ order=order,
+ distinct_on=distinct_on,
+ default_pagination=default_pagination,
+ default_name=default_name,
+ )
self._check_conflicts(type_info)
- if has_object_definition(type_):
- return type_
+ if has_object_definition(dto):
+ return dto
if existing := self._get(type_info):
return existing
strawberry_type = strawberry.type(
- type_,
+ dto,
name=type_info.name,
is_input=type_info.graphql_type == "input",
is_interface=type_info.graphql_type == "interface",
- description=description,
+ description=description or dto.__strawchemy_definition__.description,
directives=directives,
)
- self._register_type(type_info, strawberry_type)
+ self._register(type_info, strawberry_type)
return strawberry_type
def register_enum(
self,
enum_type: type[EnumT],
- name: str | None = None,
+ dto_config: DTOConfig,
+ override: bool = False,
+ user_defined: bool = False,
+ default_name: str | None = None,
description: str | None = None,
- directives: Iterable[object] = (),
+ directives: Sequence[object] = (),
) -> type[EnumT]:
- type_name = name or f"{enum_type.__name__}Enum"
- if existing := self.namespace("enum").get(type_name):
+ type_info = self._get_type_info(
+ dto=enum_type,
+ graphql_type="enum",
+ dto_config=dto_config,
+ override=override,
+ user_defined=user_defined,
+ default_name=default_name,
+ current_node=None,
+ )
+ self._check_conflicts(type_info)
+ if existing := self._get(type_info):
return cast("type[EnumT]", existing)
- strawberry_enum_type = strawberry.enum(cls=enum_type, name=name, description=description, directives=directives)
- self.namespace("enum")[type_name] = strawberry_enum_type
- return strawberry_enum_type
+ strawberry_type = strawberry.enum(
+ cls=enum_type, name=type_info.name, description=description, directives=directives
+ )
+ self._register(type_info, strawberry_type)
+ return strawberry_type
diff --git a/src/strawchemy/utils/text.py b/src/strawchemy/utils/text.py
index 70b46398..e015d881 100644
--- a/src/strawchemy/utils/text.py
+++ b/src/strawchemy/utils/text.py
@@ -6,12 +6,7 @@
if TYPE_CHECKING:
from re import Pattern
-__all__ = (
- "camel_to_snake",
- "snake_keys",
- "snake_to_camel",
- "snake_to_lower_camel_case",
-)
+__all__ = ("camel_to_snake", "snake_keys", "snake_to_camel", "snake_to_lower_camel_case")
T = TypeVar("T", bound="Any")
diff --git a/src/strawchemy/validation/pydantic.py b/src/strawchemy/validation/pydantic.py
index 93426ebd..f72a842c 100644
--- a/src/strawchemy/validation/pydantic.py
+++ b/src/strawchemy/validation/pydantic.py
@@ -5,13 +5,13 @@
from typing import TYPE_CHECKING, Any, ClassVar
from pydantic import ValidationError
-from typing_extensions import override
+from typing_extensions import Unpack, override
from strawchemy.dto.backend.pydantic import MappedPydanticDTO, PydanticDTOBackend
from strawchemy.dto.base import ModelT
-from strawchemy.dto.strawberry import StrawchemyDTOAttributes
+from strawchemy.dto.strawberry import StrawchemyObject
from strawchemy.dto.utils import read_partial
-from strawchemy.schema.factories import InputFactory
+from strawchemy.schema.factories import MutationInputFactory
from strawchemy.schema.mutation import LocalizedErrorType, ValidationErrorType
from strawchemy.utils.text import snake_to_lower_camel_case
from strawchemy.validation.base import InputValidationError, T, ValidationProtocol
@@ -24,8 +24,9 @@
from strawchemy import Strawchemy
from strawchemy.dto.base import DTOFieldDefinition, MappedDTO, Relation
- from strawchemy.dto.types import DTOConfig, ExcludeFields, IncludeFields, Purpose
+ from strawchemy.dto.types import DTOConfig, FieldIterable, IncludeFields, Purpose
from strawchemy.repository.typing import DeclarativeT
+ from strawchemy.schema.factories._kwargs import FactoryMethodKwargs
from strawchemy.typing import GraphQLPurpose
from strawchemy.utils.graph import Node
@@ -55,12 +56,12 @@ def to_error(self, exception: ValidationError) -> ValidationErrorType:
return ValidationErrorType(errors=[self._to_localized_error(err, self.to_camel) for err in exception.errors()])
-class MappedPydanticGraphQLDTO(StrawchemyDTOAttributes, MappedPydanticDTO[ModelT]):
+class MappedPydanticGraphQLDTO(StrawchemyObject, MappedPydanticDTO[ModelT]):
__strawchemy_filter__: ClassVar[type[Any] | None] = None
__strawchemy_order_by__: ClassVar[type[Any] | None] = None
-class StrawchemyInputValidationFactory(InputFactory[MappedPydanticGraphQLDTO[Any]]):
+class StrawchemyMutationInputValidationFactory(MutationInputFactory[MappedPydanticGraphQLDTO[Any]]):
@override
def _resolve_type(
self,
@@ -84,7 +85,7 @@ def input(
*,
mode: GraphQLPurpose,
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,
@@ -104,31 +105,13 @@ def factory(
dto_config: DTOConfig = read_partial,
base: type[Any] | None = None,
name: str | None = None,
- parent_field_def: DTOFieldDefinition[DeclarativeBase, QueryableAttribute[Any]] | None = None,
- current_node: Node[Relation[Any, MappedPydanticGraphQLDTO[T]], None] | None = None,
- raise_if_no_fields: bool = False,
- tags: set[str] | None = None,
- backend_kwargs: dict[str, Any] | None = None,
- *,
- description: str | None = None,
- mode: GraphQLPurpose,
- **kwargs: Any,
+ **kwargs: Unpack[FactoryMethodKwargs],
) -> type[MappedPydanticGraphQLDTO[DeclarativeT]]:
- return super().factory(
- model,
- dto_config,
- base,
- name,
- parent_field_def,
- current_node,
- raise_if_no_fields,
- tags,
- backend_kwargs=backend_kwargs,
- description=description or f"{mode.capitalize()} validation type",
- mode=mode,
- register_type=False,
- **kwargs,
- )
+ mode = kwargs.get("mode")
+ assert mode is not None, "PydanticInputFactory.factory requires `mode`"
+ kwargs["register_type"] = False
+ kwargs["description"] = kwargs.get("description") or f"{mode.capitalize()} validation type"
+ return super().factory(model, dto_config, base, name, **kwargs)
class PydanticMapper:
@@ -148,7 +131,7 @@ def __init__(self, strawchemy: Strawchemy) -> None:
pydantic_backend = PydanticDTOBackend(MappedPydanticGraphQLDTO)
self._strawchemy: Strawchemy = strawchemy
"""The Strawchemy instance used for schema introspection."""
- self._validation_factory: StrawchemyInputValidationFactory = StrawchemyInputValidationFactory(
+ self._validation_factory: StrawchemyMutationInputValidationFactory = StrawchemyMutationInputValidationFactory(
self._strawchemy, pydantic_backend
)
"""Factory for creating input validation Pydantic models."""
diff --git a/tasks.md b/tasks.md
index a0f38fe8..e877e6a8 100644
--- a/tasks.md
+++ b/tasks.md
@@ -139,50 +139,44 @@ Run slotscheck
- Depends: uv:install
-- **Usage**: `test [test]`
+- **Usage**: `test [test]…`
- **Aliases**: `t`
Run tests
### Arguments
-#### `[test]`
-
-**Default:** ``
+#### `[test]…`
## `test:coverage`
- Depends: uv:install
-- **Usage**: `test:coverage [test]`
+- **Usage**: `test:coverage [test]…`
- **Aliases**: `tc`
Run tests with coverage
### Arguments
-#### `[test]`
-
-**Default:** ``
+#### `[test]…`
## `test:integration`
- Depends: uv:install
-- **Usage**: `test:integration [--python [python]] …`
+- **Usage**: `test:integration [--python ] [test]…`
- **Aliases**: `ti`
Run integration tests
### Arguments
-#### `…`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
@@ -190,20 +184,18 @@ Run integration tests
- Depends: uv:install
-- **Usage**: `test:integration-all [--python [python]] [test]`
+- **Usage**: `test:integration-all [--python ] [test]…`
- **Aliases**: `tia`
Run integration tests on all supported python versions
### Arguments
-#### `[test]`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
@@ -211,20 +203,18 @@ Run integration tests on all supported python versions
- Depends: uv:install
-- **Usage**: `test:integration-mysql [--python [python]] …`
+- **Usage**: `test:integration-mysql [--python ] [test]…`
- **Aliases**: `ti-mysql`
Run integration tests
### Arguments
-#### `…`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
@@ -232,20 +222,18 @@ Run integration tests
- Depends: uv:install
-- **Usage**: `test:integration-postgres [--python [python]] …`
+- **Usage**: `test:integration-postgres [--python ] [test]…`
- **Aliases**: `ti-postgres`
Run integration tests
### Arguments
-#### `…`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
@@ -253,20 +241,18 @@ Run integration tests
- Depends: uv:install
-- **Usage**: `test:integration-sqlite [--python [python]] …`
+- **Usage**: `test:integration-sqlite [--python ] [test]…`
- **Aliases**: `ti-sqlite`
Run integration tests
### Arguments
-#### `…`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
@@ -274,20 +260,18 @@ Run integration tests
- Depends: uv:install
-- **Usage**: `test:integration:coverage [--python [python]] [test]`
+- **Usage**: `test:integration:coverage [--python ] [test]…`
- **Aliases**: `tic`
Run integration tests with coverage
### Arguments
-#### `[test]`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
@@ -295,20 +279,18 @@ Run integration tests with coverage
- Depends: uv:install
-- **Usage**: `test:unit [--python [python]] [test]`
+- **Usage**: `test:unit [--python ] [test]…`
- **Aliases**: `tu`
Run unit tests
### Arguments
-#### `[test]`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
@@ -316,20 +298,18 @@ Run unit tests
- Depends: uv:install
-- **Usage**: `test:unit-all [--python [python]] [test]`
+- **Usage**: `test:unit-all [--python ] [test]…`
- **Aliases**: `tua`
Run unit tests on all supported python versions
### Arguments
-#### `[test]`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
@@ -337,20 +317,18 @@ Run unit tests on all supported python versions
- Depends: uv:install
-- **Usage**: `test:unit:coverage [--python [python]] [test]`
+- **Usage**: `test:unit:coverage [--python ] [test]…`
- **Aliases**: `tuc`
Run unit tests with coverage
### Arguments
-#### `[test]`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
@@ -358,20 +336,18 @@ Run unit tests with coverage
- Depends: uv:install
-- **Usage**: `test:unit:no-extras [--python [python]] …`
+- **Usage**: `test:unit:no-extras [--python ] [test]…`
- **Aliases**: `tug`
Run unit tests without extras dependencies
### Arguments
-#### `…`
-
-**Default:** ``
+#### `[test]…`
### Flags
-#### `--python [python]`
+#### `--python `
**Default:** `3.13`
diff --git a/tests/integration/types/mysql.py b/tests/integration/types/mysql.py
index 910e6b98..88929c07 100644
--- a/tests/integration/types/mysql.py
+++ b/tests/integration/types/mysql.py
@@ -151,7 +151,7 @@ class OrderedFruitType: ...
class FruitAggregationType: ...
-@strawchemy.type(Fruit, include="all", child_pagination=True, child_order_by=True)
+@strawchemy.type(Fruit, include="all", paginate="all", order="all")
class FruitTypeWithPaginationAndOrderBy: ...
@@ -159,7 +159,7 @@ class FruitTypeWithPaginationAndOrderBy: ...
class FruitFilter: ...
-@strawchemy.order(Fruit, include="all", override=True)
+@strawchemy.order(Fruit, include="all", scope="schema")
class FruitOrderBy: ...
@@ -182,7 +182,7 @@ class FruitUpsertConflictFields: ...
# Color
-@strawchemy.type(Color, include="all", override=True, child_order_by=True)
+@strawchemy.type(Color, include="all", override=True, order="all")
class ColorType: ...
@@ -194,7 +194,7 @@ class ColorOrder: ...
class ColorDistinctOn: ...
-@strawchemy.type(Color, include="all", child_pagination=True)
+@strawchemy.type(Color, include="all", paginate="all")
class ColorTypeWithPagination: ...
diff --git a/tests/integration/types/postgres.py b/tests/integration/types/postgres.py
index 12c688a7..7ae82d6c 100644
--- a/tests/integration/types/postgres.py
+++ b/tests/integration/types/postgres.py
@@ -159,7 +159,7 @@ class OrderedFruitType: ...
class FruitAggregationType: ...
-@strawchemy.type(Fruit, include="all", child_pagination=True, child_order_by=True)
+@strawchemy.type(Fruit, include="all", paginate="all", order="all")
class FruitTypeWithPaginationAndOrderBy: ...
@@ -167,7 +167,7 @@ class FruitTypeWithPaginationAndOrderBy: ...
class FruitFilter: ...
-@strawchemy.order(Fruit, include="all", override=True)
+@strawchemy.order(Fruit, include="all", scope="schema")
class FruitOrderBy: ...
@@ -190,7 +190,7 @@ class FruitUpsertConflictFields: ...
# Color
-@strawchemy.type(Color, include="all", override=True, child_order_by=True)
+@strawchemy.type(Color, include="all", override=True, order="all")
class ColorType: ...
@@ -202,7 +202,7 @@ class ColorOrder: ...
class ColorDistinctOn: ...
-@strawchemy.type(Color, include="all", child_pagination=True)
+@strawchemy.type(Color, include="all", paginate="all")
class ColorTypeWithPagination: ...
diff --git a/tests/integration/types/sqlite.py b/tests/integration/types/sqlite.py
index d090a8e5..14561788 100644
--- a/tests/integration/types/sqlite.py
+++ b/tests/integration/types/sqlite.py
@@ -150,7 +150,7 @@ class OrderedFruitType: ...
class FruitAggregationType: ...
-@strawchemy.type(Fruit, include="all", child_pagination=True, child_order_by=True)
+@strawchemy.type(Fruit, include="all", paginate="all", order="all")
class FruitTypeWithPaginationAndOrderBy: ...
@@ -158,7 +158,7 @@ class FruitTypeWithPaginationAndOrderBy: ...
class FruitFilter: ...
-@strawchemy.order(Fruit, include="all", override=True)
+@strawchemy.order(Fruit, include="all", scope="schema")
class FruitOrderBy: ...
@@ -181,7 +181,7 @@ class FruitUpsertConflictFields: ...
# Color
-@strawchemy.type(Color, include="all", override=True, child_order_by=True)
+@strawchemy.type(Color, include="all", override=True, order="all")
class ColorType: ...
@@ -193,7 +193,7 @@ class ColorOrder: ...
class ColorDistinctOn: ...
-@strawchemy.type(Color, include="all", child_pagination=True)
+@strawchemy.type(Color, include="all", paginate="all")
class ColorTypeWithPagination: ...
diff --git a/tests/unit/__snapshots__/test_example_app/test_graphql_schema.gql b/tests/unit/__snapshots__/test_example_app/test_graphql_schema.gql
index f6714914..e9169407 100644
--- a/tests/unit/__snapshots__/test_example_app/test_graphql_schema.gql
+++ b/tests/unit/__snapshots__/test_example_app/test_graphql_schema.gql
@@ -61,19 +61,23 @@ input CustomerAggregateBoolExpSum {
distinct: Boolean = false
}
+"""GraphQL type"""
input CustomerAggregateMinMaxDatetimeFieldsOrderBy {
- createdAt: OrderByEnum!
- updatedAt: OrderByEnum!
+ createdAt: OrderByEnum
+ updatedAt: OrderByEnum
}
+"""GraphQL type"""
input CustomerAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input CustomerAggregateNumericFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input CustomerAggregateOrderBy {
count: OrderByEnum
maxDatetime: CustomerAggregateMinMaxDatetimeFieldsOrderBy
@@ -128,9 +132,7 @@ enum CustomerMinMaxStringFieldsEnum {
name
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input CustomerOrderBy {
projectsAggregate: ProjectAggregateOrderBy
projects: ProjectOrderBy
@@ -327,19 +329,23 @@ input MilestoneAggregateBoolExpSum {
distinct: Boolean = false
}
+"""GraphQL type"""
input MilestoneAggregateMinMaxDatetimeFieldsOrderBy {
- createdAt: OrderByEnum!
- updatedAt: OrderByEnum!
+ createdAt: OrderByEnum
+ updatedAt: OrderByEnum
}
+"""GraphQL type"""
input MilestoneAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input MilestoneAggregateNumericFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input MilestoneAggregateOrderBy {
count: OrderByEnum
maxDatetime: MilestoneAggregateMinMaxDatetimeFieldsOrderBy
@@ -396,9 +402,7 @@ enum MilestoneMinMaxStringFieldsEnum {
name
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input MilestoneOrderBy {
projectAggregate: ProjectAggregateOrderBy
project: ProjectOrderBy
@@ -451,12 +455,7 @@ enum MilestoneSumFieldsEnum {
"""GraphQL type"""
type MilestoneType {
- project: ProjectType
name: String!
- projectId: UUID
- id: UUID!
- createdAt: DateTime!
- updatedAt: DateTime!
}
type Mutation {
@@ -562,19 +561,23 @@ input ProjectAggregateBoolExpSum {
distinct: Boolean = false
}
+"""GraphQL type"""
input ProjectAggregateMinMaxDatetimeFieldsOrderBy {
- createdAt: OrderByEnum!
- updatedAt: OrderByEnum!
+ createdAt: OrderByEnum
+ updatedAt: OrderByEnum
}
+"""GraphQL type"""
input ProjectAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input ProjectAggregateNumericFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input ProjectAggregateOrderBy {
count: OrderByEnum
maxDatetime: ProjectAggregateMinMaxDatetimeFieldsOrderBy
@@ -657,9 +660,7 @@ enum ProjectMinMaxStringFieldsEnum {
name
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input ProjectOrder {
ticketsAggregate: TicketAggregateOrderBy
tickets: TicketOrder
@@ -676,9 +677,7 @@ input ProjectOrder {
updatedAt: OrderByEnum
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input ProjectOrderBy {
ticketsAggregate: TicketAggregateOrderBy
tickets: TicketOrder
@@ -797,19 +796,23 @@ input TagAggregateBoolExpSum {
distinct: Boolean = false
}
+"""GraphQL type"""
input TagAggregateMinMaxDatetimeFieldsOrderBy {
- createdAt: OrderByEnum!
- updatedAt: OrderByEnum!
+ createdAt: OrderByEnum
+ updatedAt: OrderByEnum
}
+"""GraphQL type"""
input TagAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input TagAggregateNumericFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input TagAggregateOrderBy {
count: OrderByEnum
maxDatetime: TagAggregateMinMaxDatetimeFieldsOrderBy
@@ -848,9 +851,7 @@ enum TagMinMaxStringFieldsEnum {
name
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input TagOrderBy {
name: OrderByEnum
id: OrderByEnum
@@ -961,19 +962,23 @@ input TicketAggregateBoolExpSum {
distinct: Boolean = false
}
+"""GraphQL type"""
input TicketAggregateMinMaxDatetimeFieldsOrderBy {
- createdAt: OrderByEnum!
- updatedAt: OrderByEnum!
+ createdAt: OrderByEnum
+ updatedAt: OrderByEnum
}
+"""GraphQL type"""
input TicketAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input TicketAggregateNumericFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input TicketAggregateOrderBy {
count: OrderByEnum
maxDatetime: TicketAggregateMinMaxDatetimeFieldsOrderBy
@@ -1030,9 +1035,7 @@ enum TicketMinMaxStringFieldsEnum {
name
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input TicketOrder {
projectAggregate: ProjectAggregateOrderBy
project: ProjectOrderBy
diff --git a/tests/unit/dto/test_dto.py b/tests/unit/dto/test_dto.py
index 455de40a..fb787dc7 100644
--- a/tests/unit/dto/test_dto.py
+++ b/tests/unit/dto/test_dto.py
@@ -5,10 +5,13 @@
from uuid import UUID, uuid4
import pytest
+from sqlalchemy import Integer
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from typing_extensions import Self
from strawchemy.dto import DTOConfig, Purpose, PurposeConfig, config, field
from strawchemy.dto.constants import DTO_INFO_KEY
+from strawchemy.dto.strawberry import DTOKey, GraphQLFieldDefinition, StrawchemyDefinition
from strawchemy.dto.utils import DTOFieldConfig, read_all_config, write_all_config
from tests.typing import AnyFactory, MappedPydanticFactory
from tests.unit.dc_models import (
@@ -23,6 +26,15 @@
from tests.utils import DTOInspect, factory_iterator
+class _PopulateFieldsBase(DeclarativeBase):
+ pass
+
+
+class _PopulateFieldsModel(_PopulateFieldsBase):
+ __tablename__ = "populate_fields_model"
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+
+
def test_config_function_produces_same_default() -> None:
assert config(Purpose.READ) == DTOConfig(Purpose.READ)
@@ -266,3 +278,93 @@ def test_forward_refs_resolved(name: str, sqlalchemy_pydantic_factory: MappedPyd
],
}
)
+
+
+# Tests for DTOConfig.from_include() and is_field_included()
+
+
+def test_from_include_with_none() -> None:
+ """Test that from_include(None) creates a config with empty include set."""
+ config = DTOConfig.from_include(None)
+ assert config.include == set()
+ assert config.purpose == Purpose.READ
+
+
+def test_from_include_with_all() -> None:
+ """Test that from_include('all') creates a config with include='all'."""
+ config = DTOConfig.from_include("all")
+ assert config.include == "all"
+ assert config.purpose == Purpose.READ
+
+
+def test_from_include_with_list() -> None:
+ """Test that from_include() accepts a list and converts it to the include parameter."""
+ config = DTOConfig.from_include(["field1", "field2"])
+ assert config.include == ["field1", "field2"]
+ assert config.purpose == Purpose.READ
+
+
+def test_from_include_with_set() -> None:
+ """Test that from_include() accepts a set for the include parameter."""
+ config = DTOConfig.from_include({"field1", "field2"})
+ assert config.include == {"field1", "field2"}
+ assert config.purpose == Purpose.READ
+
+
+def test_from_include_with_custom_purpose() -> None:
+ """Test that from_include() accepts a custom purpose."""
+ config = DTOConfig.from_include(["field1"], purpose=Purpose.WRITE)
+ assert config.include == ["field1"]
+ assert config.purpose == Purpose.WRITE
+
+
+def test_is_field_included_with_all() -> None:
+ """Test that is_field_included() returns True for any field when include='all'."""
+ config = DTOConfig.from_include("all")
+ assert config.is_field_included("any_field") is True
+ assert config.is_field_included("another_field") is True
+
+
+def test_is_field_included_with_specific_list() -> None:
+ """Test that is_field_included() returns True only for listed fields."""
+ config = DTOConfig.from_include(["field1", "field2"])
+ assert config.is_field_included("field1") is True
+ assert config.is_field_included("field2") is True
+ assert config.is_field_included("field3") is False
+
+
+def test_is_field_included_with_empty_include() -> None:
+ """Test that is_field_included() returns False for all fields when include is empty."""
+ config = DTOConfig.from_include(None)
+ assert config.is_field_included("field1") is False
+ assert config.is_field_included("any_field") is False
+
+
+def test_is_field_included_with_exclude() -> None:
+ """Test that excluded fields are properly excluded even when include='all'."""
+ config = DTOConfig(Purpose.READ, include="all", exclude={"field2", "field3"})
+ assert config.is_field_included("field1") is True
+ assert config.is_field_included("field2") is False
+ assert config.is_field_included("field3") is False
+ assert config.is_field_included("field4") is True
+
+
+@pytest.mark.parametrize(
+ "key_source",
+ [_PopulateFieldsModel, DTOKey([_PopulateFieldsModel])],
+ ids=["model-type", "dto-key"],
+)
+def test_strawchemy_definition_populate_fields(key_source: type[DeclarativeBase] | DTOKey) -> None:
+ field_def = GraphQLFieldDefinition(
+ config=DTOFieldConfig(),
+ dto_config=DTOConfig(Purpose.READ),
+ model=_PopulateFieldsModel,
+ model_field_name="id",
+ type_hint=int,
+ )
+
+ definition = StrawchemyDefinition()
+ result = definition.populate_fields(key_source, [field_def])
+
+ assert result is definition
+ assert definition.field_map == {DTOKey([_PopulateFieldsModel]) + "id": field_def}
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_mutation_schemas[create_no_id].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_mutation_schemas[create_no_id].gql
index a5d08009..8b0a257e 100644
--- a/tests/unit/mapping/__snapshots__/test_schemas/test_mutation_schemas[create_no_id].gql
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_mutation_schemas[create_no_id].gql
@@ -205,6 +205,7 @@ input GroupInput {
users: GroupUsersIdFieldsInputGroupUserInputGroupUsersUpdateFieldsGroupUsersConflictFieldsToManyCreateInput
color: GroupColorIdFieldsInputGroupColorInputGroupColorUpdateFieldsGroupColorConflictFieldsRequiredToOneInput!
name: String!
+ id: UUID
}
"""GraphQL type"""
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[all_fields_order_by].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[all_fields_order_by].gql
index efccf7e8..4e80b62e 100644
--- a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[all_fields_order_by].gql
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[all_fields_order_by].gql
@@ -1,12 +1,15 @@
'''
+"""GraphQL type"""
input ColorAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input ColorAggregateNumericFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input ColorAggregateOrderBy {
count: OrderByEnum
maxString: ColorAggregateMinMaxStringFieldsOrderBy
@@ -14,9 +17,7 @@ input ColorAggregateOrderBy {
sum: ColorAggregateNumericFieldsOrderBy
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input ColorOrderBy {
fruitsAggregate: FruitAggregateOrderBy
fruits: FruitOrderBy
@@ -47,14 +48,17 @@ type FruitAggregate {
varSamp: FruitNumericFields!
}
+"""GraphQL type"""
input FruitAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum!
+ name: OrderByEnum
}
+"""GraphQL type"""
input FruitAggregateNumericFieldsOrderBy {
- sweetness: OrderByEnum!
+ sweetness: OrderByEnum
}
+"""GraphQL type"""
input FruitAggregateOrderBy {
avg: FruitAggregateNumericFieldsOrderBy
count: OrderByEnum
@@ -80,9 +84,7 @@ type FruitNumericFields {
sweetness: Float
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input FruitOrderBy {
colorAggregate: ColorAggregateOrderBy
color: ColorOrderBy
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[argument_override].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[argument_override].gql
index 133285f6..17100eab 100644
--- a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[argument_override].gql
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[argument_override].gql
@@ -1,12 +1,15 @@
'''
+"""GraphQL type"""
input ColorAggregateMinMaxStringFieldsOrderBy {
name: OrderByEnum
}
+"""GraphQL type"""
input ColorAggregateNumericFieldsOrderBy {
name: OrderByEnum
}
+"""GraphQL type"""
input ColorAggregateOrderBy {
count: OrderByEnum
maxString: ColorAggregateMinMaxStringFieldsOrderBy
@@ -14,9 +17,7 @@ input ColorAggregateOrderBy {
sum: ColorAggregateNumericFieldsOrderBy
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input ColorOrderBy {
fruitsAggregate: FruitAggregateOrderBy
fruits: FruitOrderBy
@@ -47,14 +48,17 @@ type FruitAggregate {
varSamp: FruitNumericFields!
}
+"""GraphQL type"""
input FruitAggregateMinMaxStringFieldsOrderBy {
name: OrderByEnum
}
+"""GraphQL type"""
input FruitAggregateNumericFieldsOrderBy {
sweetness: OrderByEnum
}
+"""GraphQL type"""
input FruitAggregateOrderBy {
avg: FruitAggregateNumericFieldsOrderBy
count: OrderByEnum
@@ -80,9 +84,7 @@ type FruitNumericFields {
sweetness: Float
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input FruitOrderBy {
override: Boolean! = true
colorAggregate: ColorAggregateOrderBy
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[auto_order_by].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[auto_order_by].gql
deleted file mode 100644
index 32d165b0..00000000
--- a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[auto_order_by].gql
+++ /dev/null
@@ -1,339 +0,0 @@
-'''
-input ColorAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum
-}
-
-input ColorAggregateNumericFieldsOrderBy {
- name: OrderByEnum
-}
-
-input ColorAggregateOrderBy {
- count: OrderByEnum
- maxString: ColorAggregateMinMaxStringFieldsOrderBy
- minString: ColorAggregateMinMaxStringFieldsOrderBy
- sum: ColorAggregateNumericFieldsOrderBy
-}
-
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
-input ColorOrderBy {
- fruitsAggregate: FruitAggregateOrderBy
- fruits: FruitOrderBy
- name: OrderByEnum
- id: OrderByEnum
-}
-
-"""GraphQL type"""
-type ColorType {
- fruitsAggregate: FruitAggregate!
-
- """Fetch objects from the FruitType collection"""
- fruits(orderBy: [FruitOrderBy!] = null): [FruitType!]!
- name: String!
- id: UUID!
-}
-
-"""Aggregation fields"""
-type DepartmentAggregate {
- count: Int
- max: DepartmentMinMaxFields!
- min: DepartmentMinMaxFields!
- sum: DepartmentSumFields!
-}
-
-input DepartmentAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum
-}
-
-input DepartmentAggregateNumericFieldsOrderBy {
- name: OrderByEnum
-}
-
-input DepartmentAggregateOrderBy {
- count: OrderByEnum
- maxString: DepartmentAggregateMinMaxStringFieldsOrderBy
- minString: DepartmentAggregateMinMaxStringFieldsOrderBy
- sum: DepartmentAggregateNumericFieldsOrderBy
-}
-
-"""GraphQL type"""
-type DepartmentMinMaxFields {
- name: String
-}
-
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
-input DepartmentOrderBy {
- usersAggregate: UserAggregateOrderBy
- users: UserOrderBy
- name: OrderByEnum
- id: OrderByEnum
-}
-
-"""GraphQL type"""
-type DepartmentSumFields {
- name: String
-}
-
-"""GraphQL type"""
-type DepartmentType {
- usersAggregate: UserAggregate!
-
- """Fetch objects from the UserType collection"""
- users(orderBy: [UserOrderBy!] = null): [UserType!]!
- name: String
- id: UUID!
-}
-
-"""Aggregation fields"""
-type FruitAggregate {
- avg: FruitNumericFields!
- count: Int
- max: FruitMinMaxFields!
- min: FruitMinMaxFields!
- stddevPop: FruitNumericFields!
- stddevSamp: FruitNumericFields!
- sum: FruitSumFields!
- varPop: FruitNumericFields!
- varSamp: FruitNumericFields!
-}
-
-input FruitAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum
-}
-
-input FruitAggregateNumericFieldsOrderBy {
- sweetness: OrderByEnum
-}
-
-input FruitAggregateOrderBy {
- avg: FruitAggregateNumericFieldsOrderBy
- count: OrderByEnum
- max: FruitAggregateNumericFieldsOrderBy
- maxString: FruitAggregateMinMaxStringFieldsOrderBy
- min: FruitAggregateNumericFieldsOrderBy
- minString: FruitAggregateMinMaxStringFieldsOrderBy
- stddevPop: FruitAggregateNumericFieldsOrderBy
- stddevSamp: FruitAggregateNumericFieldsOrderBy
- sum: FruitAggregateNumericFieldsOrderBy
- varPop: FruitAggregateNumericFieldsOrderBy
- varSamp: FruitAggregateNumericFieldsOrderBy
-}
-
-"""GraphQL type"""
-type FruitMinMaxFields {
- name: String
- sweetness: Int
-}
-
-"""GraphQL type"""
-type FruitNumericFields {
- sweetness: Float
-}
-
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
-input FruitOrderBy {
- colorAggregate: ColorAggregateOrderBy
- color: ColorOrderBy
- name: OrderByEnum
- colorId: OrderByEnum
- sweetness: OrderByEnum
- id: OrderByEnum
-}
-
-"""GraphQL type"""
-type FruitSumFields {
- name: String
- sweetness: Int
-}
-
-"""GraphQL type"""
-type FruitType {
- color: ColorType!
- name: String!
- colorId: UUID
- sweetness: Int!
- id: UUID!
-}
-
-"""Aggregation fields"""
-type GroupAggregate {
- count: Int
- max: GroupMinMaxFields!
- min: GroupMinMaxFields!
- sum: GroupSumFields!
-}
-
-input GroupAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum
-}
-
-input GroupAggregateNumericFieldsOrderBy {
- name: OrderByEnum
-}
-
-input GroupAggregateOrderBy {
- count: OrderByEnum
- maxString: GroupAggregateMinMaxStringFieldsOrderBy
- minString: GroupAggregateMinMaxStringFieldsOrderBy
- sum: GroupAggregateNumericFieldsOrderBy
-}
-
-"""GraphQL type"""
-type GroupMinMaxFields {
- name: String
-}
-
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
-input GroupOrderBy {
- tagAggregate: TagAggregateOrderBy
- tag: TagOrderBy
- usersAggregate: UserAggregateOrderBy
- users: UserOrderBy
- colorAggregate: ColorAggregateOrderBy
- color: ColorOrderBy
- name: OrderByEnum
- tagId: OrderByEnum
- colorId: OrderByEnum
- id: OrderByEnum
-}
-
-"""GraphQL type"""
-type GroupSumFields {
- name: String
-}
-
-"""GraphQL type"""
-type GroupType {
- tag: TagType!
- usersAggregate: UserAggregate!
-
- """Fetch objects from the UserType collection"""
- users(orderBy: [UserOrderBy!] = null): [UserType!]!
- color: ColorType!
- name: String!
- tagId: UUID!
- colorId: UUID!
- id: UUID!
-}
-
-enum OrderByEnum {
- ASC
- ASC_NULLS_FIRST
- ASC_NULLS_LAST
- DESC
- DESC_NULLS_FIRST
- DESC_NULLS_LAST
-}
-
-type Query {
- """Fetch objects from the GroupType collection"""
- group: [GroupType!]!
-}
-
-input TagAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum
-}
-
-input TagAggregateNumericFieldsOrderBy {
- name: OrderByEnum
-}
-
-input TagAggregateOrderBy {
- count: OrderByEnum
- maxString: TagAggregateMinMaxStringFieldsOrderBy
- minString: TagAggregateMinMaxStringFieldsOrderBy
- sum: TagAggregateNumericFieldsOrderBy
-}
-
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
-input TagOrderBy {
- groupsAggregate: GroupAggregateOrderBy
- groups: GroupOrderBy
- name: OrderByEnum
- id: OrderByEnum
-}
-
-"""GraphQL type"""
-type TagType {
- groupsAggregate: GroupAggregate!
-
- """Fetch objects from the GroupType collection"""
- groups(orderBy: [GroupOrderBy!] = null): [GroupType!]!
- name: String!
- id: UUID!
-}
-
-scalar UUID
-
-"""Aggregation fields"""
-type UserAggregate {
- count: Int
- max: UserMinMaxFields!
- min: UserMinMaxFields!
- sum: UserSumFields!
-}
-
-input UserAggregateMinMaxStringFieldsOrderBy {
- name: OrderByEnum
-}
-
-input UserAggregateNumericFieldsOrderBy {
- name: OrderByEnum
-}
-
-input UserAggregateOrderBy {
- count: OrderByEnum
- maxString: UserAggregateMinMaxStringFieldsOrderBy
- minString: UserAggregateMinMaxStringFieldsOrderBy
- sum: UserAggregateNumericFieldsOrderBy
-}
-
-"""GraphQL type"""
-type UserMinMaxFields {
- name: String
-}
-
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
-input UserOrderBy {
- groupAggregate: GroupAggregateOrderBy
- group: GroupOrderBy
- tagAggregate: TagAggregateOrderBy
- tag: TagOrderBy
- departmentsAggregate: DepartmentAggregateOrderBy
- departments: DepartmentOrderBy
- name: OrderByEnum
- groupId: OrderByEnum
- tagId: OrderByEnum
- id: OrderByEnum
-}
-
-"""GraphQL type"""
-type UserSumFields {
- name: String
-}
-
-"""GraphQL type"""
-type UserType {
- group: GroupType!
- tag: TagType!
- departmentsAggregate: DepartmentAggregate!
-
- """Fetch objects from the DepartmentType collection"""
- departments(orderBy: [DepartmentOrderBy!] = null): [DepartmentType!]!
- name: String!
- groupId: UUID
- tagId: UUID
- id: UUID!
-}
-'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_all].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_all].gql
new file mode 100644
index 00000000..e858f495
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_all].gql
@@ -0,0 +1,69 @@
+'''
+enum ColorDistinctOnFields {
+ name
+ id
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(distinctOn: [FruitDistinctOnFields!] = null): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+enum FruitDistinctOnFields {
+ name
+ colorId
+ sweetness
+ id
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ColorType collection"""
+ colors(distinctOn: [ColorDistinctOnFields!] = null): [ColorType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_empty].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_empty].gql
new file mode 100644
index 00000000..4121398a
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_empty].gql
@@ -0,0 +1,57 @@
+'''
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_specific_fields].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_specific_fields].gql
new file mode 100644
index 00000000..b0ee37ba
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_specific_fields].gql
@@ -0,0 +1,65 @@
+'''
+enum ColorDistinctOnFields {
+ name
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(distinctOn: [FruitDistinctOnFields!] = null): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+enum FruitDistinctOnFields {
+ name
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ColorType collection"""
+ colors(distinctOn: [ColorDistinctOnFields!] = null): [ColorType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_with_field_override].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_with_field_override].gql
new file mode 100644
index 00000000..e858f495
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct_config_with_field_override].gql
@@ -0,0 +1,69 @@
+'''
+enum ColorDistinctOnFields {
+ name
+ id
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(distinctOn: [FruitDistinctOnFields!] = null): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+enum FruitDistinctOnFields {
+ name
+ colorId
+ sweetness
+ id
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ColorType collection"""
+ colors(distinctOn: [ColorDistinctOnFields!] = null): [ColorType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_distinct_all].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_distinct_all].gql
new file mode 100644
index 00000000..2a8310bc
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_distinct_all].gql
@@ -0,0 +1,62 @@
+'''
+enum ColorDistinctOnFields {
+ name
+ id
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ColorType collection"""
+ colors(distinctOn: [ColorDistinctOnFields!] = null): [ColorType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_distinct_specific_fields].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_distinct_specific_fields].gql
new file mode 100644
index 00000000..ef46c7b2
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_distinct_specific_fields].gql
@@ -0,0 +1,61 @@
+'''
+enum ColorDistinctOnFields {
+ name
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ColorType collection"""
+ colors(distinctOn: [ColorDistinctOnFields!] = null): [ColorType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_filter_auto_generate].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_filter_auto_generate].gql
new file mode 100644
index 00000000..68f44ab4
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_filter_auto_generate].gql
@@ -0,0 +1,324 @@
+'''
+"""
+Boolean expression to compare aggregated fields. All fields are combined with logical 'AND'.
+"""
+input ColorAggregateBoolExp {
+ count: ColorAggregateBoolExpCount
+ maxString: ColorAggregateBoolExpMaxstring
+ minString: ColorAggregateBoolExpMinstring
+ sum: ColorAggregateBoolExpSum
+}
+
+"""Boolean expression to compare count aggregation."""
+input ColorAggregateBoolExpCount {
+ arguments: [ColorCountFields!] = []
+ predicate: IntOrderComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare max aggregation."""
+input ColorAggregateBoolExpMaxstring {
+ arguments: [ColorMinMaxStringFieldsEnum!]!
+ predicate: TextComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare min aggregation."""
+input ColorAggregateBoolExpMinstring {
+ arguments: [ColorMinMaxStringFieldsEnum!]!
+ predicate: TextComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare sum aggregation."""
+input ColorAggregateBoolExpSum {
+ arguments: [ColorSumFieldsEnum!]!
+ predicate: FloatOrderComparison!
+ distinct: Boolean = false
+}
+
+"""
+Boolean expression to compare fields. All fields are combined with logical 'AND'.
+"""
+input ColorBoolExp {
+ _and: [ColorBoolExp!]! = []
+ _or: [ColorBoolExp!]! = []
+ _not: ColorBoolExp
+ fruitsAggregate: FruitAggregateBoolExp
+ fruits: FruitBoolExp
+ name: TextComparison
+ id: UUIDGenericComparison
+}
+
+enum ColorCountFields {
+ name
+ id
+}
+
+enum ColorMinMaxStringFieldsEnum {
+ name
+}
+
+enum ColorSumFieldsEnum {
+ name
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""
+Boolean expression to compare fields supporting order comparisons. All fields are combined with logical 'AND'
+"""
+input FloatOrderComparison {
+ eq: Float
+ neq: Float
+ isNull: Boolean
+ in: [Float!]
+ nin: [Float!]
+ gt: Float
+ gte: Float
+ lt: Float
+ lte: Float
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""
+Boolean expression to compare aggregated fields. All fields are combined with logical 'AND'.
+"""
+input FruitAggregateBoolExp {
+ avg: FruitAggregateBoolExpAvg
+ count: FruitAggregateBoolExpCount
+ max: FruitAggregateBoolExpMax
+ maxString: FruitAggregateBoolExpMaxstring
+ min: FruitAggregateBoolExpMin
+ minString: FruitAggregateBoolExpMinstring
+ stddevPop: FruitAggregateBoolExpStddevpop
+ stddevSamp: FruitAggregateBoolExpStddevsamp
+ sum: FruitAggregateBoolExpSum
+ varPop: FruitAggregateBoolExpVarpop
+ varSamp: FruitAggregateBoolExpVarsamp
+}
+
+"""Boolean expression to compare avg aggregation."""
+input FruitAggregateBoolExpAvg {
+ arguments: [FruitNumericFieldsEnum!]!
+ predicate: FloatOrderComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare count aggregation."""
+input FruitAggregateBoolExpCount {
+ arguments: [FruitCountFields!] = []
+ predicate: IntOrderComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare max aggregation."""
+input FruitAggregateBoolExpMax {
+ arguments: [FruitMinMaxNumericFieldsEnum!]!
+ predicate: FloatOrderComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare max aggregation."""
+input FruitAggregateBoolExpMaxstring {
+ arguments: [FruitMinMaxStringFieldsEnum!]!
+ predicate: TextComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare min aggregation."""
+input FruitAggregateBoolExpMin {
+ arguments: [FruitMinMaxNumericFieldsEnum!]!
+ predicate: FloatOrderComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare min aggregation."""
+input FruitAggregateBoolExpMinstring {
+ arguments: [FruitMinMaxStringFieldsEnum!]!
+ predicate: TextComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare stddev_pop aggregation."""
+input FruitAggregateBoolExpStddevpop {
+ arguments: [FruitNumericFieldsEnum!]!
+ predicate: FloatOrderComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare stddev_samp aggregation."""
+input FruitAggregateBoolExpStddevsamp {
+ arguments: [FruitNumericFieldsEnum!]!
+ predicate: FloatOrderComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare sum aggregation."""
+input FruitAggregateBoolExpSum {
+ arguments: [FruitSumFieldsEnum!]!
+ predicate: FloatOrderComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare var_pop aggregation."""
+input FruitAggregateBoolExpVarpop {
+ arguments: [FruitNumericFieldsEnum!]!
+ predicate: FloatOrderComparison!
+ distinct: Boolean = false
+}
+
+"""Boolean expression to compare var_samp aggregation."""
+input FruitAggregateBoolExpVarsamp {
+ arguments: [FruitNumericFieldsEnum!]!
+ predicate: FloatOrderComparison!
+ distinct: Boolean = false
+}
+
+"""
+Boolean expression to compare fields. All fields are combined with logical 'AND'.
+"""
+input FruitBoolExp {
+ _and: [FruitBoolExp!]! = []
+ _or: [FruitBoolExp!]! = []
+ _not: FruitBoolExp
+ colorAggregate: ColorAggregateBoolExp
+ color: ColorBoolExp
+ name: TextComparison
+ colorId: UUIDGenericComparison
+ sweetness: IntOrderComparison
+ id: UUIDGenericComparison
+}
+
+enum FruitCountFields {
+ name
+ colorId
+ sweetness
+ id
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+enum FruitMinMaxNumericFieldsEnum {
+ sweetness
+}
+
+enum FruitMinMaxStringFieldsEnum {
+ name
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+enum FruitNumericFieldsEnum {
+ sweetness
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+enum FruitSumFieldsEnum {
+ name
+ sweetness
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+"""
+Boolean expression to compare fields supporting order comparisons. All fields are combined with logical 'AND'
+"""
+input IntOrderComparison {
+ eq: Int
+ neq: Int
+ isNull: Boolean
+ in: [Int!]
+ nin: [Int!]
+ gt: Int
+ gte: Int
+ lt: Int
+ lte: Int
+}
+
+type Query {
+ """Fetch objects from the FruitType collection"""
+ fruits(filter: FruitBoolExp = null): [FruitType!]!
+}
+
+"""
+Boolean expression to compare String fields. All fields are combined with logical 'AND'
+"""
+input TextComparison {
+ eq: String
+ neq: String
+ isNull: Boolean
+ in: [String!]
+ nin: [String!]
+ gt: String
+ gte: String
+ lt: String
+ lte: String
+ like: String
+ nlike: String
+ ilike: String
+ nilike: String
+ regexp: String
+ iregexp: String
+ nregexp: String
+ inregexp: String
+ startswith: String
+ endswith: String
+ contains: String
+ istartswith: String
+ iendswith: String
+ icontains: String
+}
+
+scalar UUID
+
+"""
+Boolean expression to compare fields supporting equality comparisons. All fields are combined with logical 'AND'
+"""
+input UUIDGenericComparison {
+ eq: UUID
+ neq: UUID
+ isNull: Boolean
+ in: [UUID!]
+ nin: [UUID!]
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by].gql
index 3e027528..f3f737eb 100644
--- a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by].gql
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by].gql
@@ -32,9 +32,7 @@ type Query {
sqlDataTypes(orderBy: [SQLDataTypesOrderBy!] = null): [SQLDataTypesType!]!
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input SQLDataTypesOrderBy {
dateCol: OrderByEnum
timeCol: OrderByEnum
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by_all].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by_all].gql
new file mode 100644
index 00000000..608a6c7a
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by_all].gql
@@ -0,0 +1,137 @@
+'''
+"""GraphQL type"""
+input ColorAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateNumericFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateOrderBy {
+ count: OrderByEnum
+ maxString: ColorAggregateMinMaxStringFieldsOrderBy
+ minString: ColorAggregateMinMaxStringFieldsOrderBy
+ sum: ColorAggregateNumericFieldsOrderBy
+}
+
+"""Ordering input."""
+input ColorOrderBy {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+ name: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+input FruitAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateNumericFieldsOrderBy {
+ sweetness: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateOrderBy {
+ avg: FruitAggregateNumericFieldsOrderBy
+ count: OrderByEnum
+ max: FruitAggregateNumericFieldsOrderBy
+ maxString: FruitAggregateMinMaxStringFieldsOrderBy
+ min: FruitAggregateNumericFieldsOrderBy
+ minString: FruitAggregateMinMaxStringFieldsOrderBy
+ stddevPop: FruitAggregateNumericFieldsOrderBy
+ stddevSamp: FruitAggregateNumericFieldsOrderBy
+ sum: FruitAggregateNumericFieldsOrderBy
+ varPop: FruitAggregateNumericFieldsOrderBy
+ varSamp: FruitAggregateNumericFieldsOrderBy
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""Ordering input."""
+input FruitOrderBy {
+ colorAggregate: ColorAggregateOrderBy
+ color: ColorOrderBy
+ name: OrderByEnum
+ colorId: OrderByEnum
+ sweetness: OrderByEnum
+ id: OrderByEnum
+}
+
+"""Ordering input."""
+input FruitOrderBy1 {
+ colorAggregate: ColorAggregateOrderBy
+ color: ColorOrderBy
+ name: OrderByEnum
+ colorId: OrderByEnum
+ sweetness: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+enum OrderByEnum {
+ ASC
+ ASC_NULLS_FIRST
+ ASC_NULLS_LAST
+ DESC
+ DESC_NULLS_FIRST
+ DESC_NULLS_LAST
+}
+
+type Query {
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy1!] = null): [FruitType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by_specific_fields].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by_specific_fields].gql
new file mode 100644
index 00000000..14ac7e96
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[field_order_by_specific_fields].gql
@@ -0,0 +1,220 @@
+'''
+"""Aggregation fields"""
+type ColorAggregate {
+ count: Int
+ max: ColorMinMaxFields!
+ min: ColorMinMaxFields!
+ sum: ColorSumFields!
+}
+
+"""GraphQL type"""
+input ColorAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateNumericFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateOrderBy {
+ count: OrderByEnum
+ maxString: ColorAggregateMinMaxStringFieldsOrderBy
+ minString: ColorAggregateMinMaxStringFieldsOrderBy
+ sum: ColorAggregateNumericFieldsOrderBy
+}
+
+"""GraphQL type"""
+type ColorMinMaxFields {
+ name: String
+}
+
+"""Ordering input."""
+input ColorOrderBy {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+ name: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type ColorSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Ordering input."""
+input ContainerOrderBy1 {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+ vegetablesAggregate: VegetableAggregateOrderBy
+ vegetables: VegetableOrderBy
+}
+
+"""GraphQL type"""
+type ContainerType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables: [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+input FruitAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateNumericFieldsOrderBy {
+ sweetness: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateOrderBy {
+ avg: FruitAggregateNumericFieldsOrderBy
+ count: OrderByEnum
+ max: FruitAggregateNumericFieldsOrderBy
+ maxString: FruitAggregateMinMaxStringFieldsOrderBy
+ min: FruitAggregateNumericFieldsOrderBy
+ minString: FruitAggregateMinMaxStringFieldsOrderBy
+ stddevPop: FruitAggregateNumericFieldsOrderBy
+ stddevSamp: FruitAggregateNumericFieldsOrderBy
+ sum: FruitAggregateNumericFieldsOrderBy
+ varPop: FruitAggregateNumericFieldsOrderBy
+ varSamp: FruitAggregateNumericFieldsOrderBy
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""Ordering input."""
+input FruitOrderBy {
+ colorAggregate: ColorAggregateOrderBy
+ color: ColorOrderBy
+ name: OrderByEnum
+ colorId: OrderByEnum
+ sweetness: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+enum OrderByEnum {
+ ASC
+ ASC_NULLS_FIRST
+ ASC_NULLS_LAST
+ DESC
+ DESC_NULLS_FIRST
+ DESC_NULLS_LAST
+}
+
+type Query {
+ """Fetch objects from the ContainerType collection"""
+ containers(orderBy: [ContainerOrderBy1!] = null): [ContainerType!]!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type VegetableAggregate {
+ count: Int
+ max: VegetableMinMaxFields!
+ min: VegetableMinMaxFields!
+ sum: VegetableSumFields!
+}
+
+"""GraphQL type"""
+input VegetableAggregateOrderBy {
+ count: OrderByEnum
+}
+
+enum VegetableFamily {
+ MUSHROOM
+ GOURD
+ CABBAGE
+ ONION
+ SEEDS
+}
+
+"""GraphQL type"""
+type VegetableMinMaxFields {
+ name: String
+ description: String
+}
+
+"""Ordering input."""
+input VegetableOrderBy {
+ family: OrderByEnum
+ id: OrderByEnum
+ name: OrderByEnum
+ description: OrderByEnum
+}
+
+"""GraphQL type"""
+type VegetableSumFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableType {
+ family: VegetableFamily!
+ id: UUID!
+ name: String!
+ description: String!
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_all].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_all].gql
new file mode 100644
index 00000000..e72f02ef
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_all].gql
@@ -0,0 +1,137 @@
+'''
+"""GraphQL type"""
+input ColorAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateNumericFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateOrderBy {
+ count: OrderByEnum
+ maxString: ColorAggregateMinMaxStringFieldsOrderBy
+ minString: ColorAggregateMinMaxStringFieldsOrderBy
+ sum: ColorAggregateNumericFieldsOrderBy
+}
+
+"""Ordering input."""
+input ColorOrderBy {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+ name: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy!] = null): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+input FruitAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateNumericFieldsOrderBy {
+ sweetness: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateOrderBy {
+ avg: FruitAggregateNumericFieldsOrderBy
+ count: OrderByEnum
+ max: FruitAggregateNumericFieldsOrderBy
+ maxString: FruitAggregateMinMaxStringFieldsOrderBy
+ min: FruitAggregateNumericFieldsOrderBy
+ minString: FruitAggregateMinMaxStringFieldsOrderBy
+ stddevPop: FruitAggregateNumericFieldsOrderBy
+ stddevSamp: FruitAggregateNumericFieldsOrderBy
+ sum: FruitAggregateNumericFieldsOrderBy
+ varPop: FruitAggregateNumericFieldsOrderBy
+ varSamp: FruitAggregateNumericFieldsOrderBy
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""Ordering input."""
+input FruitOrderBy {
+ colorAggregate: ColorAggregateOrderBy
+ color: ColorOrderBy
+ name: OrderByEnum
+ colorId: OrderByEnum
+ sweetness: OrderByEnum
+ id: OrderByEnum
+}
+
+"""Ordering input."""
+input FruitOrderBy2 {
+ colorAggregate: ColorAggregateOrderBy
+ color: ColorOrderBy
+ name: OrderByEnum
+ colorId: OrderByEnum
+ sweetness: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+enum OrderByEnum {
+ ASC
+ ASC_NULLS_FIRST
+ ASC_NULLS_LAST
+ DESC
+ DESC_NULLS_FIRST
+ DESC_NULLS_LAST
+}
+
+type Query {
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy2!] = null): [FruitType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_all_with_field_override].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_all_with_field_override].gql
new file mode 100644
index 00000000..c7a489d7
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_all_with_field_override].gql
@@ -0,0 +1,132 @@
+'''
+"""GraphQL type"""
+input ColorAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateNumericFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateOrderBy {
+ count: OrderByEnum
+ maxString: ColorAggregateMinMaxStringFieldsOrderBy
+ minString: ColorAggregateMinMaxStringFieldsOrderBy
+ sum: ColorAggregateNumericFieldsOrderBy
+}
+
+"""Ordering input."""
+input ColorOrderBy {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+ name: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy!] = null): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+input FruitAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateNumericFieldsOrderBy {
+ sweetness: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateOrderBy {
+ avg: FruitAggregateNumericFieldsOrderBy
+ count: OrderByEnum
+ max: FruitAggregateNumericFieldsOrderBy
+ maxString: FruitAggregateMinMaxStringFieldsOrderBy
+ min: FruitAggregateNumericFieldsOrderBy
+ minString: FruitAggregateMinMaxStringFieldsOrderBy
+ stddevPop: FruitAggregateNumericFieldsOrderBy
+ stddevSamp: FruitAggregateNumericFieldsOrderBy
+ sum: FruitAggregateNumericFieldsOrderBy
+ varPop: FruitAggregateNumericFieldsOrderBy
+ varSamp: FruitAggregateNumericFieldsOrderBy
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""Ordering input."""
+input FruitOrderBy {
+ colorAggregate: ColorAggregateOrderBy
+ color: ColorOrderBy
+ name: OrderByEnum
+ colorId: OrderByEnum
+ sweetness: OrderByEnum
+ id: OrderByEnum
+}
+
+"""Ordering input."""
+input FruitOrderBy2 {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+enum OrderByEnum {
+ ASC
+ ASC_NULLS_FIRST
+ ASC_NULLS_LAST
+ DESC
+ DESC_NULLS_FIRST
+ DESC_NULLS_LAST
+}
+
+type Query {
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy2!] = null): [FruitType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_empty].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_empty].gql
new file mode 100644
index 00000000..918eed85
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_empty].gql
@@ -0,0 +1,57 @@
+'''
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_empty_with_empty_type_override].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_empty_with_empty_type_override].gql
new file mode 100644
index 00000000..03fdedc2
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_empty_with_empty_type_override].gql
@@ -0,0 +1,129 @@
+'''
+"""Aggregation fields"""
+type ColorAggregate {
+ count: Int
+ max: ColorMinMaxFields!
+ min: ColorMinMaxFields!
+ sum: ColorSumFields!
+}
+
+"""GraphQL type"""
+type ColorMinMaxFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type ContainerType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables: [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ContainerType collection"""
+ container: [ContainerType!]!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type VegetableAggregate {
+ count: Int
+ max: VegetableMinMaxFields!
+ min: VegetableMinMaxFields!
+ sum: VegetableSumFields!
+}
+
+enum VegetableFamily {
+ MUSHROOM
+ GOURD
+ CABBAGE
+ ONION
+ SEEDS
+}
+
+"""GraphQL type"""
+type VegetableMinMaxFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableSumFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableType {
+ family: VegetableFamily!
+ id: UUID!
+ name: String!
+ description: String!
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_specific_fields].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_specific_fields].gql
new file mode 100644
index 00000000..9ff9c1be
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_specific_fields].gql
@@ -0,0 +1,78 @@
+'''
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy4!] = null): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""Ordering input."""
+input FruitOrderBy2 {
+ name: OrderByEnum
+ sweetness: OrderByEnum
+}
+
+"""Ordering input."""
+input FruitOrderBy4 {
+ name: OrderByEnum
+ sweetness: OrderByEnum
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+enum OrderByEnum {
+ ASC
+ ASC_NULLS_FIRST
+ ASC_NULLS_LAST
+ DESC
+ DESC_NULLS_FIRST
+ DESC_NULLS_LAST
+}
+
+type Query {
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy2!] = null): [FruitType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_specific_fields_with_type_override].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_specific_fields_with_type_override].gql
new file mode 100644
index 00000000..1f7af4c9
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_specific_fields_with_type_override].gql
@@ -0,0 +1,219 @@
+'''
+"""Aggregation fields"""
+type ColorAggregate {
+ count: Int
+ max: ColorMinMaxFields!
+ min: ColorMinMaxFields!
+ sum: ColorSumFields!
+}
+
+"""GraphQL type"""
+input ColorAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateNumericFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateOrderBy {
+ count: OrderByEnum
+ maxString: ColorAggregateMinMaxStringFieldsOrderBy
+ minString: ColorAggregateMinMaxStringFieldsOrderBy
+ sum: ColorAggregateNumericFieldsOrderBy
+}
+
+"""GraphQL type"""
+type ColorMinMaxFields {
+ name: String
+}
+
+"""Ordering input."""
+input ColorOrderBy {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+ name: OrderByEnum
+ id: OrderByEnum
+}
+
+"""Ordering input."""
+input ColorOrderBy2 {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+}
+
+"""GraphQL type"""
+type ColorSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy!] = null): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Ordering input."""
+input ContainerOrderBy2 {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+}
+
+"""GraphQL type"""
+type ContainerType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy!] = null): [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables(orderBy: [VegetableOrderBy!] = null): [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors(orderBy: [ColorOrderBy2!] = null): [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+input FruitAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateNumericFieldsOrderBy {
+ sweetness: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateOrderBy {
+ avg: FruitAggregateNumericFieldsOrderBy
+ count: OrderByEnum
+ max: FruitAggregateNumericFieldsOrderBy
+ maxString: FruitAggregateMinMaxStringFieldsOrderBy
+ min: FruitAggregateNumericFieldsOrderBy
+ minString: FruitAggregateMinMaxStringFieldsOrderBy
+ stddevPop: FruitAggregateNumericFieldsOrderBy
+ stddevSamp: FruitAggregateNumericFieldsOrderBy
+ sum: FruitAggregateNumericFieldsOrderBy
+ varPop: FruitAggregateNumericFieldsOrderBy
+ varSamp: FruitAggregateNumericFieldsOrderBy
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""Ordering input."""
+input FruitOrderBy {
+ colorAggregate: ColorAggregateOrderBy
+ color: ColorOrderBy
+ name: OrderByEnum
+ colorId: OrderByEnum
+ sweetness: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+enum OrderByEnum {
+ ASC
+ ASC_NULLS_FIRST
+ ASC_NULLS_LAST
+ DESC
+ DESC_NULLS_FIRST
+ DESC_NULLS_LAST
+}
+
+type Query {
+ """Fetch objects from the ContainerType collection"""
+ container(orderBy: [ContainerOrderBy2!] = null): [ContainerType!]!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type VegetableAggregate {
+ count: Int
+ max: VegetableMinMaxFields!
+ min: VegetableMinMaxFields!
+ sum: VegetableSumFields!
+}
+
+enum VegetableFamily {
+ MUSHROOM
+ GOURD
+ CABBAGE
+ ONION
+ SEEDS
+}
+
+"""GraphQL type"""
+type VegetableMinMaxFields {
+ name: String
+ description: String
+}
+
+"""Ordering input."""
+input VegetableOrderBy {
+ family: OrderByEnum
+ id: OrderByEnum
+ name: OrderByEnum
+ description: OrderByEnum
+}
+
+"""GraphQL type"""
+type VegetableSumFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableType {
+ family: VegetableFamily!
+ id: UUID!
+ name: String!
+ description: String!
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_with_field_override].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_with_field_override].gql
new file mode 100644
index 00000000..bcc6481e
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[order_config_with_field_override].gql
@@ -0,0 +1,76 @@
+'''
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy4!] = null): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""Ordering input."""
+input FruitOrderBy2 {
+ sweetness: OrderByEnum
+}
+
+"""Ordering input."""
+input FruitOrderBy4 {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+enum OrderByEnum {
+ ASC
+ ASC_NULLS_FIRST
+ ASC_NULLS_LAST
+ DESC
+ DESC_NULLS_FIRST
+ DESC_NULLS_LAST
+}
+
+type Query {
+ """Fetch objects from the FruitType collection"""
+ fruits(orderBy: [FruitOrderBy2!] = null): [FruitType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_all_with_default].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_all_with_default].gql
new file mode 100644
index 00000000..1540a4c6
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_all_with_default].gql
@@ -0,0 +1,57 @@
+'''
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 20, offset: Int! = 5): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the FruitType collection"""
+ fruitWithCustomDefault: [FruitType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_and_order_combined].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_and_order_combined].gql
new file mode 100644
index 00000000..35a2d58d
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_and_order_combined].gql
@@ -0,0 +1,272 @@
+'''
+"""Aggregation fields"""
+type ColorAggregate {
+ count: Int
+ max: ColorMinMaxFields!
+ min: ColorMinMaxFields!
+ sum: ColorSumFields!
+}
+
+"""GraphQL type"""
+input ColorAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateNumericFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input ColorAggregateOrderBy {
+ count: OrderByEnum
+ maxString: ColorAggregateMinMaxStringFieldsOrderBy
+ minString: ColorAggregateMinMaxStringFieldsOrderBy
+ sum: ColorAggregateNumericFieldsOrderBy
+}
+
+"""GraphQL type"""
+type ColorMinMaxFields {
+ name: String
+}
+
+"""Ordering input."""
+input ColorOrderBy {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+ name: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type ColorSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Ordering input."""
+input ContainerOrderBy {
+ vegetablesAggregate: VegetableAggregateOrderBy
+ vegetables: VegetableOrderBy
+}
+
+"""Ordering input."""
+input ContainerOrderBy1 {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+}
+
+"""Ordering input."""
+input ContainerOrderBy2 {
+ fruitsAggregate: FruitAggregateOrderBy
+ fruits: FruitOrderBy
+}
+
+"""GraphQL type"""
+type ContainerType1 {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 0): [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables(orderBy: [VegetableOrderBy!] = null): [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type ContainerType2 {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 0, orderBy: [FruitOrderBy!] = null): [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables(limit: Int = 100, offset: Int! = 0): [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type ContainerType3 {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 0, orderBy: [FruitOrderBy!] = null): [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables(limit: Int = 100, offset: Int! = 0): [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors(limit: Int = 100, offset: Int! = 0): [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+input FruitAggregateMinMaxStringFieldsOrderBy {
+ name: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateNumericFieldsOrderBy {
+ sweetness: OrderByEnum
+}
+
+"""GraphQL type"""
+input FruitAggregateOrderBy {
+ avg: FruitAggregateNumericFieldsOrderBy
+ count: OrderByEnum
+ max: FruitAggregateNumericFieldsOrderBy
+ maxString: FruitAggregateMinMaxStringFieldsOrderBy
+ min: FruitAggregateNumericFieldsOrderBy
+ minString: FruitAggregateMinMaxStringFieldsOrderBy
+ stddevPop: FruitAggregateNumericFieldsOrderBy
+ stddevSamp: FruitAggregateNumericFieldsOrderBy
+ sum: FruitAggregateNumericFieldsOrderBy
+ varPop: FruitAggregateNumericFieldsOrderBy
+ varSamp: FruitAggregateNumericFieldsOrderBy
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""Ordering input."""
+input FruitOrderBy {
+ colorAggregate: ColorAggregateOrderBy
+ color: ColorOrderBy
+ name: OrderByEnum
+ colorId: OrderByEnum
+ sweetness: OrderByEnum
+ id: OrderByEnum
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+enum OrderByEnum {
+ ASC
+ ASC_NULLS_FIRST
+ ASC_NULLS_LAST
+ DESC
+ DESC_NULLS_FIRST
+ DESC_NULLS_LAST
+}
+
+type Query {
+ """Fetch objects from the ContainerType1 collection"""
+ container1(orderBy: [ContainerOrderBy!] = null): [ContainerType1!]!
+
+ """Fetch objects from the ContainerType2 collection"""
+ container2(orderBy: [ContainerOrderBy1!] = null): [ContainerType2!]!
+
+ """Fetch objects from the ContainerType3 collection"""
+ container3(orderBy: [ContainerOrderBy2!] = null): [ContainerType3!]!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type VegetableAggregate {
+ count: Int
+ max: VegetableMinMaxFields!
+ min: VegetableMinMaxFields!
+ sum: VegetableSumFields!
+}
+
+"""GraphQL type"""
+input VegetableAggregateOrderBy {
+ count: OrderByEnum
+}
+
+enum VegetableFamily {
+ MUSHROOM
+ GOURD
+ CABBAGE
+ ONION
+ SEEDS
+}
+
+"""GraphQL type"""
+type VegetableMinMaxFields {
+ name: String
+ description: String
+}
+
+"""Ordering input."""
+input VegetableOrderBy {
+ family: OrderByEnum
+ id: OrderByEnum
+ name: OrderByEnum
+ description: OrderByEnum
+}
+
+"""GraphQL type"""
+type VegetableSumFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableType {
+ family: VegetableFamily!
+ id: UUID!
+ name: String!
+ description: String!
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_empty].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_empty].gql
new file mode 100644
index 00000000..03fdedc2
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_empty].gql
@@ -0,0 +1,129 @@
+'''
+"""Aggregation fields"""
+type ColorAggregate {
+ count: Int
+ max: ColorMinMaxFields!
+ min: ColorMinMaxFields!
+ sum: ColorSumFields!
+}
+
+"""GraphQL type"""
+type ColorMinMaxFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type ContainerType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables: [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ContainerType collection"""
+ container: [ContainerType!]!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type VegetableAggregate {
+ count: Int
+ max: VegetableMinMaxFields!
+ min: VegetableMinMaxFields!
+ sum: VegetableSumFields!
+}
+
+enum VegetableFamily {
+ MUSHROOM
+ GOURD
+ CABBAGE
+ ONION
+ SEEDS
+}
+
+"""GraphQL type"""
+type VegetableMinMaxFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableSumFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableType {
+ family: VegetableFamily!
+ id: UUID!
+ name: String!
+ description: String!
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_specific_fields].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_specific_fields].gql
new file mode 100644
index 00000000..7c9a3730
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[paginate_specific_fields].gql
@@ -0,0 +1,129 @@
+'''
+"""Aggregation fields"""
+type ColorAggregate {
+ count: Int
+ max: ColorMinMaxFields!
+ min: ColorMinMaxFields!
+ sum: ColorSumFields!
+}
+
+"""GraphQL type"""
+type ColorMinMaxFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type ContainerType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 0): [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables: [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ContainerType collection"""
+ container: [ContainerType!]!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type VegetableAggregate {
+ count: Int
+ max: VegetableMinMaxFields!
+ min: VegetableMinMaxFields!
+ sum: VegetableSumFields!
+}
+
+enum VegetableFamily {
+ MUSHROOM
+ GOURD
+ CABBAGE
+ ONION
+ SEEDS
+}
+
+"""GraphQL type"""
+type VegetableMinMaxFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableSumFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableType {
+ family: VegetableFamily!
+ id: UUID!
+ name: String!
+ description: String!
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_default].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_default].gql
index 9bf20f23..0b4346cf 100644
--- a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_default].gql
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_default].gql
@@ -4,7 +4,7 @@ type ColorType {
fruitsAggregate: FruitAggregate!
"""Fetch objects from the FruitType collection"""
- fruits: [FruitType!]!
+ fruits(limit: Int = 100, offset: Int! = 0): [FruitType!]!
name: String!
id: UUID!
}
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_empty].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_empty].gql
new file mode 100644
index 00000000..03fdedc2
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_empty].gql
@@ -0,0 +1,129 @@
+'''
+"""Aggregation fields"""
+type ColorAggregate {
+ count: Int
+ max: ColorMinMaxFields!
+ min: ColorMinMaxFields!
+ sum: ColorSumFields!
+}
+
+"""GraphQL type"""
+type ColorMinMaxFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type ContainerType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables: [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ContainerType collection"""
+ container: [ContainerType!]!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type VegetableAggregate {
+ count: Int
+ max: VegetableMinMaxFields!
+ min: VegetableMinMaxFields!
+ sum: VegetableSumFields!
+}
+
+enum VegetableFamily {
+ MUSHROOM
+ GOURD
+ CABBAGE
+ ONION
+ SEEDS
+}
+
+"""GraphQL type"""
+type VegetableMinMaxFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableSumFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableType {
+ family: VegetableFamily!
+ id: UUID!
+ name: String!
+ description: String!
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_specific_fields].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_specific_fields].gql
new file mode 100644
index 00000000..e9c2d7ed
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_specific_fields].gql
@@ -0,0 +1,129 @@
+'''
+"""Aggregation fields"""
+type ColorAggregate {
+ count: Int
+ max: ColorMinMaxFields!
+ min: ColorMinMaxFields!
+ sum: ColorSumFields!
+}
+
+"""GraphQL type"""
+type ColorMinMaxFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 0): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type ContainerType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 0): [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables(limit: Int = 100, offset: Int! = 0): [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ContainerType collection"""
+ container: [ContainerType!]!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type VegetableAggregate {
+ count: Int
+ max: VegetableMinMaxFields!
+ min: VegetableMinMaxFields!
+ sum: VegetableSumFields!
+}
+
+enum VegetableFamily {
+ MUSHROOM
+ GOURD
+ CABBAGE
+ ONION
+ SEEDS
+}
+
+"""GraphQL type"""
+type VegetableMinMaxFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableSumFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableType {
+ family: VegetableFamily!
+ id: UUID!
+ name: String!
+ description: String!
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_with_type_override].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_with_type_override].gql
new file mode 100644
index 00000000..e9c2d7ed
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_config_with_type_override].gql
@@ -0,0 +1,129 @@
+'''
+"""Aggregation fields"""
+type ColorAggregate {
+ count: Int
+ max: ColorMinMaxFields!
+ min: ColorMinMaxFields!
+ sum: ColorSumFields!
+}
+
+"""GraphQL type"""
+type ColorMinMaxFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 0): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type ContainerType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 0): [FruitType!]!
+ vegetablesAggregate: VegetableAggregate!
+
+ """Fetch objects from the VegetableType collection"""
+ vegetables(limit: Int = 100, offset: Int! = 0): [VegetableType!]!
+ colorsAggregate: ColorAggregate!
+
+ """Fetch objects from the ColorType collection"""
+ colors: [ColorType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the ContainerType collection"""
+ container: [ContainerType!]!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type VegetableAggregate {
+ count: Int
+ max: VegetableMinMaxFields!
+ min: VegetableMinMaxFields!
+ sum: VegetableSumFields!
+}
+
+enum VegetableFamily {
+ MUSHROOM
+ GOURD
+ CABBAGE
+ ONION
+ SEEDS
+}
+
+"""GraphQL type"""
+type VegetableMinMaxFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableSumFields {
+ name: String
+ description: String
+}
+
+"""GraphQL type"""
+type VegetableType {
+ family: VegetableFamily!
+ id: UUID!
+ name: String!
+ description: String!
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_default_offset].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_default_offset].gql
new file mode 100644
index 00000000..01352f38
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[pagination_default_offset].gql
@@ -0,0 +1,57 @@
+'''
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 5): [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+type Query {
+ """Fetch objects from the FruitType collection"""
+ fruits(limit: Int = 100, offset: Int! = 5): [FruitType!]!
+}
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[scope_schema_in_the_middle].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[scope_schema_in_the_middle].gql
index dd7d6d50..3694f14a 100644
--- a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[scope_schema_in_the_middle].gql
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[scope_schema_in_the_middle].gql
@@ -132,6 +132,20 @@ type GroupSumFields {
name: String
}
+"""GraphQL type"""
+type GroupType {
+ tag: TagType!
+ usersAggregate: UserAggregate!
+
+ """Fetch objects from the GraphQLUser collection"""
+ users: [GraphQLUser!]!
+ color: ColorType!
+ name: String!
+ tagId: UUID!
+ colorId: UUID!
+ id: UUID!
+}
+
type Query {
"""Fetch object from the GraphQLUser collection by id"""
user(id: UUID!): GraphQLUser!
@@ -144,8 +158,8 @@ type Query {
type TagType {
groupsAggregate: GroupAggregate!
- """Fetch objects from the GraphQLGroup collection"""
- groups: [GraphQLGroup!]!
+ """Fetch objects from the GroupType collection"""
+ groups: [GroupType!]!
name: String!
id: UUID!
}
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[scope_schema_middle].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[scope_schema_middle].gql
new file mode 100644
index 00000000..dd7d6d50
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[scope_schema_middle].gql
@@ -0,0 +1,172 @@
+'''
+"""GraphQL type"""
+type ColorType {
+ fruitsAggregate: FruitAggregate!
+
+ """Fetch objects from the FruitType collection"""
+ fruits: [FruitType!]!
+ name: String!
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type DepartmentAggregate {
+ count: Int
+ max: DepartmentMinMaxFields!
+ min: DepartmentMinMaxFields!
+ sum: DepartmentSumFields!
+}
+
+"""GraphQL type"""
+type DepartmentMinMaxFields {
+ name: String
+}
+
+"""GraphQL type"""
+type DepartmentSumFields {
+ name: String
+}
+
+"""GraphQL type"""
+type DepartmentType {
+ usersAggregate: UserAggregate!
+
+ """Fetch objects from the GraphQLUser collection"""
+ users: [GraphQLUser!]!
+ name: String
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type FruitAggregate {
+ avg: FruitNumericFields!
+ count: Int
+ max: FruitMinMaxFields!
+ min: FruitMinMaxFields!
+ stddevPop: FruitNumericFields!
+ stddevSamp: FruitNumericFields!
+ sum: FruitSumFields!
+ varPop: FruitNumericFields!
+ varSamp: FruitNumericFields!
+}
+
+"""GraphQL type"""
+type FruitMinMaxFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitNumericFields {
+ sweetness: Float
+}
+
+"""GraphQL type"""
+type FruitSumFields {
+ name: String
+ sweetness: Int
+}
+
+"""GraphQL type"""
+type FruitType {
+ color: ColorType!
+ name: String!
+ colorId: UUID
+ sweetness: Int!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type GraphQLGroup {
+ tag: TagType!
+ usersAggregate: UserAggregate!
+
+ """Fetch objects from the GraphQLUser collection"""
+ users: [GraphQLUser!]!
+ color: ColorType!
+ name: String!
+ tagId: UUID!
+ colorId: UUID!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type GraphQLTag {
+ groupsAggregate: GroupAggregate!
+
+ """Fetch objects from the GraphQLGroup collection"""
+ groups: [GraphQLGroup!]!
+ name: String!
+ id: UUID!
+}
+
+"""GraphQL type"""
+type GraphQLUser {
+ group: GraphQLGroup!
+ tag: TagType!
+ departmentsAggregate: DepartmentAggregate!
+
+ """Fetch objects from the DepartmentType collection"""
+ departments: [DepartmentType!]!
+ name: String!
+ groupId: UUID
+ tagId: UUID
+ id: UUID!
+}
+
+"""Aggregation fields"""
+type GroupAggregate {
+ count: Int
+ max: GroupMinMaxFields!
+ min: GroupMinMaxFields!
+ sum: GroupSumFields!
+}
+
+"""GraphQL type"""
+type GroupMinMaxFields {
+ name: String
+}
+
+"""GraphQL type"""
+type GroupSumFields {
+ name: String
+}
+
+type Query {
+ """Fetch object from the GraphQLUser collection by id"""
+ user(id: UUID!): GraphQLUser!
+
+ """Fetch object from the GraphQLTag collection by id"""
+ tag(id: UUID!): GraphQLTag!
+}
+
+"""GraphQL type"""
+type TagType {
+ groupsAggregate: GroupAggregate!
+
+ """Fetch objects from the GraphQLGroup collection"""
+ groups: [GraphQLGroup!]!
+ name: String!
+ id: UUID!
+}
+
+scalar UUID
+
+"""Aggregation fields"""
+type UserAggregate {
+ count: Int
+ max: UserMinMaxFields!
+ min: UserMinMaxFields!
+ sum: UserSumFields!
+}
+
+"""GraphQL type"""
+type UserMinMaxFields {
+ name: String
+}
+
+"""GraphQL type"""
+type UserSumFields {
+ name: String
+}
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[type_distinct_manual_enum].gql
similarity index 100%
rename from tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[distinct].gql
rename to tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[type_distinct_manual_enum].gql
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[type_order_by].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[type_order_by].gql
index 3e027528..f3f737eb 100644
--- a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[type_order_by].gql
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[type_order_by].gql
@@ -32,9 +32,7 @@ type Query {
sqlDataTypes(orderBy: [SQLDataTypesOrderBy!] = null): [SQLDataTypesType!]!
}
-"""
-Boolean expression to compare fields. All fields are combined with logical 'AND'.
-"""
+"""Ordering input."""
input SQLDataTypesOrderBy {
dateCol: OrderByEnum
timeCol: OrderByEnum
diff --git a/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[type_order_by_specific_fields].gql b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[type_order_by_specific_fields].gql
new file mode 100644
index 00000000..f7e5dd49
--- /dev/null
+++ b/tests/unit/mapping/__snapshots__/test_schemas/test_query_schemas[type_order_by_specific_fields].gql
@@ -0,0 +1,62 @@
+'''
+"""Date (isoformat)"""
+scalar Date
+
+"""Date with time (isoformat)"""
+scalar DateTime
+
+"""Decimal (fixed-point)"""
+scalar Decimal
+
+"""
+The `Interval` scalar type represents a duration of time as specified by [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations).
+"""
+scalar Interval @specifiedBy(url: "https://en.wikipedia.org/wiki/ISO_8601#Durations")
+
+"""
+The `JSON` scalar type represents JSON values as specified by [ECMA-404](https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf).
+"""
+scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf")
+
+enum OrderByEnum {
+ ASC
+ ASC_NULLS_FIRST
+ ASC_NULLS_LAST
+ DESC
+ DESC_NULLS_FIRST
+ DESC_NULLS_LAST
+}
+
+type Query {
+ """Fetch objects from the SQLDataTypesType collection"""
+ sqlDataTypes(orderBy: [SQLDataTypesOrderBy!] = null): [SQLDataTypesType!]!
+}
+
+"""Ordering input."""
+input SQLDataTypesOrderBy {
+ strCol: OrderByEnum
+ intCol: OrderByEnum
+}
+
+"""GraphQL type"""
+type SQLDataTypesType {
+ dateCol: Date!
+ timeCol: Time!
+ timeDeltaCol: Interval!
+ datetimeCol: DateTime!
+ strCol: String!
+ intCol: Int!
+ floatCol: Float!
+ decimalCol: Decimal!
+ boolCol: Boolean!
+ uuidCol: UUID!
+ dictCol(path: String): JSON
+ arrayStrCol: [String!]!
+ id: UUID!
+}
+
+"""Time (isoformat)"""
+scalar Time
+
+scalar UUID
+'''
\ No newline at end of file
diff --git a/tests/unit/mapping/test_schemas.py b/tests/unit/mapping/test_schemas.py
index ae1d08f6..8d10fd75 100644
--- a/tests/unit/mapping/test_schemas.py
+++ b/tests/unit/mapping/test_schemas.py
@@ -98,13 +98,7 @@ def test_type_resolution_with_resolvers() -> None:
[pytest.param("tests.unit.schemas.override.auto_type_existing", id="auto_type_existing")],
)
def test_multiple_types_error(path: str) -> None:
- with pytest.raises(
- StrawchemyError,
- match=re.escape(
- """Type `FruitType` cannot be auto generated because it's already declared."""
- """ You may want to set `override=True` on the existing type to use it everywhere."""
- ),
- ):
+ with pytest.raises(StrawchemyError, match=re.escape("Type `FruitType` is already registered")):
import_module(path)
@@ -184,21 +178,52 @@ def test_update_mutation_by_filter_type_not_list_fail() -> None:
pytest.param("pagination.pagination_defaults.Query", id="pagination_defaults"),
pytest.param("pagination.children_pagination.Query", id="children_pagination"),
pytest.param("pagination.children_pagination_defaults.Query", id="children_pagination_defaults"),
- pytest.param("pagination.pagination_default_limit.Query", id="pagination_default_limit"),
pytest.param("pagination.pagination_config_default.Query", id="pagination_config_default"),
+ pytest.param("pagination.pagination_default_limit.Query", id="pagination_default_limit"),
+ pytest.param("pagination.pagination_default_offset.Query", id="pagination_default_offset"),
+ pytest.param("pagination.paginate_specific_fields.Query", id="paginate_specific_fields"),
+ pytest.param("pagination.paginate_empty.Query", id="paginate_empty"),
+ pytest.param("pagination.pagination_config_empty.Query", id="pagination_config_empty"),
+ pytest.param("pagination.pagination_config_specific_fields.Query", id="pagination_config_specific_fields"),
+ pytest.param(
+ "pagination.pagination_config_with_type_override.Query", id="pagination_config_with_type_override"
+ ),
+ pytest.param("pagination.paginate_and_order_combined.Query", id="paginate_and_order_combined"),
+ pytest.param("pagination.paginate_all_with_default.Query", id="paginate_all_with_default"),
pytest.param("custom_id_field_name.Query", id="custom_id_field_name"),
pytest.param("enums.Query", id="enums"),
pytest.param("filters.filters.Query", id="filters"),
pytest.param("filters.filters_aggregation.Query", id="aggregation_filters"),
pytest.param("filters.type_filter.Query", id="type_filter"),
+ pytest.param("filters.field_filter_auto_generate.Query", id="field_filter_auto_generate"),
pytest.param("order.type_order_by.Query", id="type_order_by"),
pytest.param("order.field_order_by.Query", id="field_order_by"),
- pytest.param("order.auto_order_by.Query", id="auto_order_by"),
+ pytest.param("order.field_order_by_all.Query", id="field_order_by_all"),
+ pytest.param("order.field_order_by_specific_fields.Query", id="field_order_by_specific_fields"),
+ pytest.param("order.order_config_all.Query", id="order_config_all"),
+ pytest.param(
+ "order.order_config_specific_fields_with_type_override.Query",
+ id="order_config_specific_fields_with_type_override",
+ ),
+ pytest.param(
+ "order.order_config_empty_with_empty_type_override.Query", id="order_config_empty_with_empty_type_override"
+ ),
+ pytest.param("order.order_config_all_with_field_override.Query", id="order_config_all_with_field_override"),
+ pytest.param("order.order_config_specific_fields.Query", id="order_config_specific_fields"),
+ pytest.param("order.order_config_empty.Query", id="order_config_empty"),
+ pytest.param("order.order_config_with_field_override.Query", id="order_config_with_field_override"),
+ pytest.param("order.type_order_by_specific_fields.Query", id="type_order_by_specific_fields"),
pytest.param("aggregations.root_aggregations.Query", id="root_aggregations"),
- pytest.param("distinct.Query", id="distinct"),
+ pytest.param("distinct.type_distinct_manual_enum.Query", id="type_distinct_manual_enum"),
+ pytest.param("distinct.distinct_config_all.Query", id="distinct_config_all"),
+ pytest.param("distinct.field_distinct_all.Query", id="field_distinct_all"),
+ pytest.param("distinct.field_distinct_specific_fields.Query", id="field_distinct_specific_fields"),
+ pytest.param("distinct.distinct_config_with_field_override.Query", id="distinct_config_with_field_override"),
+ pytest.param("distinct.distinct_config_specific_fields.Query", id="distinct_config_specific_fields"),
+ pytest.param("distinct.distinct_config_empty.Query", id="distinct_config_empty"),
pytest.param("scope.schema_before.Query", id="scope_schema_before"),
pytest.param("scope.schema_after.Query", id="scope_schema_after"),
- pytest.param("scope.schema_in_the_middle.Query", id="scope_schema_in_the_middle"),
+ pytest.param("scope.schema_middle.Query", id="scope_schema_middle"),
],
)
@pytest.mark.snapshot
@@ -505,19 +530,20 @@ def test_pydantic_validation_nested() -> None:
]
-def test_schema_scope_override() -> None:
- from tests.unit.schemas.scope.schema_after import Query as QueryAfter
- from tests.unit.schemas.scope.schema_before import Query as QueryBefore
- from tests.unit.schemas.scope.schema_in_the_middle import Query as QueryInTheMiddle
-
- schema_in_the_middle = strawberry.Schema(query=QueryInTheMiddle, scalar_overrides=SCALAR_OVERRIDES)
- schema_after = strawberry.Schema(query=QueryAfter, scalar_overrides=SCALAR_OVERRIDES)
- schema_before = strawberry.Schema(query=QueryBefore, scalar_overrides=SCALAR_OVERRIDES)
+@pytest.mark.parametrize(
+ "module_name",
+ [
+ pytest.param("schema_after", id="scope-after"),
+ pytest.param("schema_before", id="scope-before"),
+ pytest.param("schema_middle", id="scope-middle"),
+ ],
+)
+def test_schema_scope_override(module_name: str) -> None:
+ """Test schema scope is working properly no matter where the override is declared."""
+ query_class = import_module(f"tests.unit.schemas.scope.{module_name}").Query
- schemas_str = [
- textwrap.dedent(str(schema)).strip() for schema in [schema_in_the_middle, schema_after, schema_before]
- ]
+ schema = strawberry.Schema(query=query_class, scalar_overrides=SCALAR_OVERRIDES)
+ schemas_str = textwrap.dedent(str(schema)).strip()
- for schema in schemas_str:
- assert "GroupType" not in schema
- assert "GraphQLGroup" in schema
+ assert "GroupType" not in schemas_str
+ assert "GraphQLGroup" in schemas_str
diff --git a/tests/unit/models.py b/tests/unit/models.py
index c2832440..a9580c9d 100644
--- a/tests/unit/models.py
+++ b/tests/unit/models.py
@@ -197,6 +197,21 @@ class SQLDataTypes(UUIDBase):
array_str_col: Mapped[list[str]] = mapped_column(postgresql.ARRAY(Text), default=list)
+class Container(UUIDBase):
+ """Test model with multiple list relationships for testing paginate/order configuration."""
+
+ __tablename__ = "container"
+
+ name: Mapped[str]
+ fruits: Mapped[list[Fruit]] = relationship(
+ "Fruit", primaryjoin="Container.id == foreign(Fruit.color_id)", viewonly=True
+ )
+ vegetables: Mapped[list[Vegetable]] = relationship(
+ "Vegetable", primaryjoin="Container.id == foreign(Vegetable.id)", viewonly=True
+ )
+ colors: Mapped[list[Color]] = relationship("Color", primaryjoin="Container.id == foreign(Color.id)", viewonly=True)
+
+
# Geo
if GEO_INSTALLED:
diff --git a/tests/unit/schemas/distinct/__init__.py b/tests/unit/schemas/distinct/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/unit/schemas/distinct/distinct_config_all.py b/tests/unit/schemas/distinct/distinct_config_all.py
new file mode 100644
index 00000000..0da45351
--- /dev/null
+++ b/tests/unit/schemas/distinct/distinct_config_all.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Color
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", distinct_on="all"))
+
+
+@strawchemy.type(Color, include="all")
+class ColorType:
+ pass
+
+
+@strawberry.type
+class Query:
+ colors: list[ColorType] = strawchemy.field()
diff --git a/tests/unit/schemas/distinct/distinct_config_empty.py b/tests/unit/schemas/distinct/distinct_config_empty.py
new file mode 100644
index 00000000..67d8fea3
--- /dev/null
+++ b/tests/unit/schemas/distinct/distinct_config_empty.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Color
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", distinct_on=[]))
+
+
+@strawchemy.type(Color, include="all")
+class ColorType:
+ pass
+
+
+@strawberry.type
+class Query:
+ colors: list[ColorType] = strawchemy.field()
diff --git a/tests/unit/schemas/distinct/distinct_config_specific_fields.py b/tests/unit/schemas/distinct/distinct_config_specific_fields.py
new file mode 100644
index 00000000..3b7431c4
--- /dev/null
+++ b/tests/unit/schemas/distinct/distinct_config_specific_fields.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Color
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", distinct_on=["name"]))
+
+
+@strawchemy.type(Color, include="all")
+class ColorType:
+ pass
+
+
+@strawberry.type
+class Query:
+ colors: list[ColorType] = strawchemy.field()
diff --git a/tests/unit/schemas/distinct/distinct_config_with_field_override.py b/tests/unit/schemas/distinct/distinct_config_with_field_override.py
new file mode 100644
index 00000000..7ea7c088
--- /dev/null
+++ b/tests/unit/schemas/distinct/distinct_config_with_field_override.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Color
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", distinct_on="all"))
+
+
+@strawchemy.type(Color, include="all")
+class ColorType:
+ pass
+
+
+@strawberry.type
+class Query:
+ colors: list[ColorType] = strawchemy.field(distinct_on=["name"])
diff --git a/tests/unit/schemas/distinct/field_distinct_all.py b/tests/unit/schemas/distinct/field_distinct_all.py
new file mode 100644
index 00000000..7a4a3acb
--- /dev/null
+++ b/tests/unit/schemas/distinct/field_distinct_all.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import Color
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(Color, include="all")
+class ColorType:
+ pass
+
+
+@strawberry.type
+class Query:
+ colors: list[ColorType] = strawchemy.field(distinct_on="all")
diff --git a/tests/unit/schemas/distinct/field_distinct_specific_fields.py b/tests/unit/schemas/distinct/field_distinct_specific_fields.py
new file mode 100644
index 00000000..b6e4b242
--- /dev/null
+++ b/tests/unit/schemas/distinct/field_distinct_specific_fields.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import Color
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(Color, include="all")
+class ColorType:
+ pass
+
+
+@strawberry.type
+class Query:
+ colors: list[ColorType] = strawchemy.field(distinct_on=["name", "hex"])
diff --git a/tests/unit/schemas/distinct.py b/tests/unit/schemas/distinct/type_distinct_manual_enum.py
similarity index 100%
rename from tests/unit/schemas/distinct.py
rename to tests/unit/schemas/distinct/type_distinct_manual_enum.py
diff --git a/tests/unit/schemas/filters/field_filter_auto_generate.py b/tests/unit/schemas/filters/field_filter_auto_generate.py
new file mode 100644
index 00000000..6f72b2cf
--- /dev/null
+++ b/tests/unit/schemas/filters/field_filter_auto_generate.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import Fruit
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(Fruit, include="all")
+class FruitType:
+ pass
+
+
+@strawberry.type
+class Query:
+ fruits: list[FruitType] = strawchemy.field(filter_input=True)
diff --git a/tests/unit/schemas/order/auto_order_by.py b/tests/unit/schemas/order/auto_order_by.py
deleted file mode 100644
index a8a96e21..00000000
--- a/tests/unit/schemas/order/auto_order_by.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from __future__ import annotations
-
-import strawberry
-
-from strawchemy import Strawchemy
-from tests.unit.models import Group
-
-strawchemy = Strawchemy("postgresql")
-
-
-@strawchemy.type(Group, include="all", child_order_by=True)
-class GroupType: ...
-
-
-@strawberry.type
-class Query:
- group: list[GroupType] = strawchemy.field()
diff --git a/tests/unit/schemas/order/field_order_by_all.py b/tests/unit/schemas/order/field_order_by_all.py
new file mode 100644
index 00000000..9aec1336
--- /dev/null
+++ b/tests/unit/schemas/order/field_order_by_all.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import Fruit
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(Fruit, include="all")
+class FruitType:
+ pass
+
+
+@strawberry.type
+class Query:
+ fruits: list[FruitType] = strawchemy.field(order_by="all")
diff --git a/tests/unit/schemas/order/field_order_by_specific_fields.py b/tests/unit/schemas/order/field_order_by_specific_fields.py
new file mode 100644
index 00000000..91c9a19a
--- /dev/null
+++ b/tests/unit/schemas/order/field_order_by_specific_fields.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import Container
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(Container, include="all")
+class ContainerType:
+ pass
+
+
+@strawberry.type
+class Query:
+ containers: list[ContainerType] = strawchemy.field(order_by=["fruits", "vegetables"])
diff --git a/tests/unit/schemas/order/order_config_all.py b/tests/unit/schemas/order/order_config_all.py
new file mode 100644
index 00000000..abaed7d4
--- /dev/null
+++ b/tests/unit/schemas/order/order_config_all.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Fruit
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", order_by="all"))
+
+
+@strawchemy.type(Fruit, include="all")
+class FruitType:
+ pass
+
+
+@strawberry.type
+class Query:
+ fruits: list[FruitType] = strawchemy.field()
diff --git a/tests/unit/schemas/order/order_config_all_with_field_override.py b/tests/unit/schemas/order/order_config_all_with_field_override.py
new file mode 100644
index 00000000..d71294b0
--- /dev/null
+++ b/tests/unit/schemas/order/order_config_all_with_field_override.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Fruit
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", order_by="all"))
+
+
+@strawchemy.type(Fruit, include="all")
+class FruitType:
+ pass
+
+
+@strawberry.type
+class Query:
+ fruits: list[FruitType] = strawchemy.field(order_by=["name"])
diff --git a/tests/unit/schemas/order/order_config_empty.py b/tests/unit/schemas/order/order_config_empty.py
new file mode 100644
index 00000000..aac1b568
--- /dev/null
+++ b/tests/unit/schemas/order/order_config_empty.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Fruit
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", order_by=[]))
+
+
+@strawchemy.type(Fruit, include="all")
+class FruitType:
+ pass
+
+
+@strawberry.type
+class Query:
+ fruits: list[FruitType] = strawchemy.field()
diff --git a/tests/unit/schemas/order/order_config_empty_with_empty_type_override.py b/tests/unit/schemas/order/order_config_empty_with_empty_type_override.py
new file mode 100644
index 00000000..11f3cbf3
--- /dev/null
+++ b/tests/unit/schemas/order/order_config_empty_with_empty_type_override.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import Container
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(Container, include="all", order=[])
+class ContainerType:
+ pass
+
+
+@strawberry.type
+class Query:
+ container: list[ContainerType] = strawchemy.field()
diff --git a/tests/unit/schemas/order/order_config_specific_fields.py b/tests/unit/schemas/order/order_config_specific_fields.py
new file mode 100644
index 00000000..b48ab41e
--- /dev/null
+++ b/tests/unit/schemas/order/order_config_specific_fields.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Fruit
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", order_by=["name", "sweetness"]))
+
+
+@strawchemy.type(Fruit, include="all")
+class FruitType:
+ pass
+
+
+@strawberry.type
+class Query:
+ fruits: list[FruitType] = strawchemy.field()
diff --git a/tests/unit/schemas/order/order_config_specific_fields_with_type_override.py b/tests/unit/schemas/order/order_config_specific_fields_with_type_override.py
new file mode 100644
index 00000000..93379108
--- /dev/null
+++ b/tests/unit/schemas/order/order_config_specific_fields_with_type_override.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Container
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", order_by=["fruits"]))
+
+
+@strawchemy.type(Container, include="all", order=["fruits", "vegetables"])
+class ContainerType:
+ pass
+
+
+@strawberry.type
+class Query:
+ container: list[ContainerType] = strawchemy.field()
diff --git a/tests/unit/schemas/order/order_config_with_field_override.py b/tests/unit/schemas/order/order_config_with_field_override.py
new file mode 100644
index 00000000..448c573f
--- /dev/null
+++ b/tests/unit/schemas/order/order_config_with_field_override.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Fruit
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", order_by=["name"]))
+
+
+@strawchemy.type(Fruit, include="all")
+class FruitType:
+ pass
+
+
+@strawberry.type
+class Query:
+ fruits: list[FruitType] = strawchemy.field(order_by=["sweetness"])
diff --git a/tests/unit/schemas/order/type_order_by.py b/tests/unit/schemas/order/type_order_by.py
index 022c8828..1fd982da 100644
--- a/tests/unit/schemas/order/type_order_by.py
+++ b/tests/unit/schemas/order/type_order_by.py
@@ -12,7 +12,7 @@
class SQLDataTypesOrderBy: ...
-@strawchemy.type(SQLDataTypes, include="all", order_by=SQLDataTypesOrderBy)
+@strawchemy.type(SQLDataTypes, include="all", order=SQLDataTypesOrderBy)
class SQLDataTypesType: ...
diff --git a/tests/unit/schemas/order/type_order_by_specific_fields.py b/tests/unit/schemas/order/type_order_by_specific_fields.py
new file mode 100644
index 00000000..075e6806
--- /dev/null
+++ b/tests/unit/schemas/order/type_order_by_specific_fields.py
@@ -0,0 +1,17 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import SQLDataTypes
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(SQLDataTypes, include="all", order=["str_col", "int_col"])
+class SQLDataTypesType: ...
+
+
+@strawberry.type
+class Query:
+ sql_data_types: list[SQLDataTypesType] = strawchemy.field()
diff --git a/tests/unit/schemas/override/override_argument.py b/tests/unit/schemas/override/override_argument.py
index a41b6520..fc76c887 100644
--- a/tests/unit/schemas/override/override_argument.py
+++ b/tests/unit/schemas/override/override_argument.py
@@ -8,7 +8,7 @@
strawchemy = Strawchemy("postgresql")
-@strawchemy.type(Fruit, include="all", child_pagination=True, child_order_by=True)
+@strawchemy.type(Fruit, include="all", paginate="all", order="all")
class FruitType:
name: int
diff --git a/tests/unit/schemas/pagination/children_pagination.py b/tests/unit/schemas/pagination/children_pagination.py
index 2bcd129d..727e6bd1 100644
--- a/tests/unit/schemas/pagination/children_pagination.py
+++ b/tests/unit/schemas/pagination/children_pagination.py
@@ -8,7 +8,7 @@
strawchemy = Strawchemy("postgresql")
-@strawchemy.type(Fruit, include="all", child_pagination=True)
+@strawchemy.type(Fruit, include="all", paginate="all")
class FruitType:
pass
diff --git a/tests/unit/schemas/pagination/children_pagination_defaults.py b/tests/unit/schemas/pagination/children_pagination_defaults.py
index 9c81a1c5..3326df77 100644
--- a/tests/unit/schemas/pagination/children_pagination_defaults.py
+++ b/tests/unit/schemas/pagination/children_pagination_defaults.py
@@ -9,7 +9,7 @@
strawchemy = Strawchemy("postgresql")
-@strawchemy.type(Fruit, include="all", child_pagination=DefaultOffsetPagination(limit=10, offset=10))
+@strawchemy.type(Fruit, include="all", paginate="all", default_pagination=DefaultOffsetPagination(limit=10, offset=10))
class FruitType:
pass
diff --git a/tests/unit/schemas/pagination/paginate_all_with_default.py b/tests/unit/schemas/pagination/paginate_all_with_default.py
new file mode 100644
index 00000000..85d70fd1
--- /dev/null
+++ b/tests/unit/schemas/pagination/paginate_all_with_default.py
@@ -0,0 +1,19 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from strawchemy.schema.pagination import DefaultOffsetPagination
+from tests.unit.models import Fruit
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(Fruit, include="all", paginate="all", default_pagination=DefaultOffsetPagination(limit=20, offset=5))
+class FruitType:
+ pass
+
+
+@strawberry.type
+class Query:
+ fruit_with_custom_default: list[FruitType] = strawchemy.field()
diff --git a/tests/unit/schemas/pagination/paginate_and_order_combined.py b/tests/unit/schemas/pagination/paginate_and_order_combined.py
new file mode 100644
index 00000000..42affe2b
--- /dev/null
+++ b/tests/unit/schemas/pagination/paginate_and_order_combined.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import Container
+
+strawchemy = Strawchemy("postgresql")
+
+
+# Different fields for paginate vs order
+@strawchemy.type(Container, include="all", paginate=["fruits"], order=["vegetables"])
+class ContainerType1:
+ pass
+
+
+# Overlapping fields
+@strawchemy.type(Container, include="all", paginate=["fruits", "vegetables"], order=["fruits"])
+class ContainerType2:
+ pass
+
+
+# All + specific
+@strawchemy.type(Container, include="all", paginate="all", order=["fruits"])
+class ContainerType3:
+ pass
+
+
+@strawberry.type
+class Query:
+ container1: list[ContainerType1] = strawchemy.field()
+ container2: list[ContainerType2] = strawchemy.field()
+ container3: list[ContainerType3] = strawchemy.field()
diff --git a/tests/unit/schemas/pagination/paginate_empty.py b/tests/unit/schemas/pagination/paginate_empty.py
new file mode 100644
index 00000000..c9d25536
--- /dev/null
+++ b/tests/unit/schemas/pagination/paginate_empty.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import Container
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(Container, include="all", paginate=[])
+class ContainerType:
+ pass
+
+
+@strawberry.type
+class Query:
+ container: list[ContainerType] = strawchemy.field()
diff --git a/tests/unit/schemas/pagination/paginate_specific_fields.py b/tests/unit/schemas/pagination/paginate_specific_fields.py
new file mode 100644
index 00000000..d829b834
--- /dev/null
+++ b/tests/unit/schemas/pagination/paginate_specific_fields.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy
+from tests.unit.models import Container
+
+strawchemy = Strawchemy("postgresql")
+
+
+@strawchemy.type(Container, include="all", paginate=["fruits"])
+class ContainerType:
+ pass
+
+
+@strawberry.type
+class Query:
+ container: list[ContainerType] = strawchemy.field()
diff --git a/tests/unit/schemas/pagination/pagination_config_default.py b/tests/unit/schemas/pagination/pagination_config_default.py
index 46134b59..559b7645 100644
--- a/tests/unit/schemas/pagination/pagination_config_default.py
+++ b/tests/unit/schemas/pagination/pagination_config_default.py
@@ -5,7 +5,7 @@
from strawchemy import Strawchemy, StrawchemyConfig
from tests.unit.models import Fruit
-strawchemy = Strawchemy(StrawchemyConfig("postgresql", pagination=True))
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", pagination="all"))
@strawchemy.type(Fruit, include="all")
diff --git a/tests/unit/schemas/pagination/pagination_config_empty.py b/tests/unit/schemas/pagination/pagination_config_empty.py
new file mode 100644
index 00000000..9d219425
--- /dev/null
+++ b/tests/unit/schemas/pagination/pagination_config_empty.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Container
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", pagination=[]))
+
+
+@strawchemy.type(Container, include="all")
+class ContainerType:
+ pass
+
+
+@strawberry.type
+class Query:
+ container: list[ContainerType] = strawchemy.field()
diff --git a/tests/unit/schemas/pagination/pagination_config_empty_with_type_override.py b/tests/unit/schemas/pagination/pagination_config_empty_with_type_override.py
new file mode 100644
index 00000000..dfd1c96c
--- /dev/null
+++ b/tests/unit/schemas/pagination/pagination_config_empty_with_type_override.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Container
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", pagination=[]))
+
+
+@strawchemy.type(Container, include="all", paginate=["fruits"])
+class ContainerType:
+ pass
+
+
+@strawberry.type
+class Query:
+ container: list[ContainerType] = strawchemy.field()
diff --git a/tests/unit/schemas/pagination/pagination_config_specific_fields.py b/tests/unit/schemas/pagination/pagination_config_specific_fields.py
new file mode 100644
index 00000000..69f7b738
--- /dev/null
+++ b/tests/unit/schemas/pagination/pagination_config_specific_fields.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Container
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", pagination=["fruits", "vegetables"]))
+
+
+@strawchemy.type(Container, include="all")
+class ContainerType:
+ pass
+
+
+@strawberry.type
+class Query:
+ container: list[ContainerType] = strawchemy.field()
diff --git a/tests/unit/schemas/pagination/pagination_config_with_type_override.py b/tests/unit/schemas/pagination/pagination_config_with_type_override.py
new file mode 100644
index 00000000..5c557728
--- /dev/null
+++ b/tests/unit/schemas/pagination/pagination_config_with_type_override.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Container
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", pagination=["fruits"]))
+
+
+@strawchemy.type(Container, include="all", paginate=["vegetables"])
+class ContainerType:
+ pass
+
+
+@strawberry.type
+class Query:
+ container: list[ContainerType] = strawchemy.field()
diff --git a/tests/unit/schemas/pagination/pagination_default_limit.py b/tests/unit/schemas/pagination/pagination_default_limit.py
index d815418c..36ac8f21 100644
--- a/tests/unit/schemas/pagination/pagination_default_limit.py
+++ b/tests/unit/schemas/pagination/pagination_default_limit.py
@@ -8,7 +8,7 @@
strawchemy = Strawchemy(StrawchemyConfig("postgresql", pagination_default_limit=5))
-@strawchemy.type(Fruit, include="all", child_pagination=True)
+@strawchemy.type(Fruit, include="all", paginate="all")
class FruitType:
pass
diff --git a/tests/unit/schemas/pagination/pagination_default_offset.py b/tests/unit/schemas/pagination/pagination_default_offset.py
new file mode 100644
index 00000000..a0719eb7
--- /dev/null
+++ b/tests/unit/schemas/pagination/pagination_default_offset.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import strawberry
+
+from strawchemy import Strawchemy, StrawchemyConfig
+from tests.unit.models import Fruit
+
+strawchemy = Strawchemy(StrawchemyConfig("postgresql", pagination_default_offset=5))
+
+
+@strawchemy.type(Fruit, include="all", paginate="all")
+class FruitType:
+ pass
+
+
+@strawberry.type
+class Query:
+ fruits: list[FruitType] = strawchemy.field(pagination=True)
diff --git a/tests/unit/schemas/scope/schema_in_the_middle.py b/tests/unit/schemas/scope/schema_middle.py
similarity index 100%
rename from tests/unit/schemas/scope/schema_in_the_middle.py
rename to tests/unit/schemas/scope/schema_middle.py
diff --git a/uv.lock b/uv.lock
index 9480417a..db8f5ab0 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2105,14 +2105,14 @@ uv = [
[[package]]
name = "nox-uv"
-version = "0.6.3"
+version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nox" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/70/af/ebc522c51facc3b7d26df1c5bc0b72baadb33ce4d539cf66758f22008f6c/nox_uv-0.6.3.tar.gz", hash = "sha256:7940dc4fed7326d00c9687d6dac65d625db21064dd02631af64cfdfa738e817b", size = 4992, upload-time = "2025-10-23T01:32:53.774Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/e8/670919c513c22f4bf1656d84dd99a9ad1a5eaaeadf2457bab3efeeac14e0/nox_uv-0.7.1.tar.gz", hash = "sha256:f075d610b4648732fd17cbc9fa48be7d2c23df7b188fed3e4e6dde7bd1f14f20", size = 5124, upload-time = "2026-02-05T03:55:34.807Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/98/cc/32317fd187febd9b2c5dc6cad4d25b358bd3c289958a00b78d94dbddf481/nox_uv-0.6.3-py3-none-any.whl", hash = "sha256:650cc4dafcde281a77d0526ac9ff9c92de5cd7759ecc9fdbf98393e4ab24490f", size = 5315, upload-time = "2025-10-23T01:32:52.977Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/0a/a6798a215366c9b034e92a9992d9013da5f544a488216fc54204ccf3c134/nox_uv-0.7.1-py3-none-any.whl", hash = "sha256:91361cc282a0a764de1b94ad002b67d5b43de4adc3f56e16d1b79928c8ec0433", size = 5457, upload-time = "2026-02-05T03:55:35.994Z" },
]
[[package]]
@@ -3301,19 +3301,16 @@ mysql = [
{ name = "asyncmy" },
{ name = "cryptography" },
]
+nox = [
+ { name = "nox", extra = ["uv"] },
+ { name = "nox-uv" },
+]
postgres = [
{ name = "asyncpg" },
{ name = "psycopg", extra = ["binary", "pool"] },
]
test = [
- { name = "aiosqlite" },
- { name = "asyncmy" },
- { name = "asyncpg" },
{ name = "covdefaults" },
- { name = "cryptography" },
- { name = "nox", extra = ["uv"] },
- { name = "nox-uv" },
- { name = "psycopg", extra = ["binary", "pool"] },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
@@ -3387,19 +3384,16 @@ mysql = [
{ name = "asyncmy" },
{ name = "cryptography" },
]
+nox = [
+ { name = "nox", extras = ["uv"] },
+ { name = "nox-uv" },
+]
postgres = [
{ name = "asyncpg", specifier = ">=0.29.0" },
{ name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.2.3" },
]
test = [
- { name = "aiosqlite" },
- { name = "asyncmy" },
- { name = "asyncpg", specifier = ">=0.29.0" },
{ name = "covdefaults" },
- { name = "cryptography" },
- { name = "nox", extras = ["uv"] },
- { name = "nox-uv" },
- { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.2.3" },
{ name = "pytest" },
{ name = "pytest-asyncio", specifier = ">=0.24" },
{ name = "pytest-cov" },