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" },