Repository: saleor/saleor

Issue:
Bug: Sorting by `RANK` when it doesn't make sense gives Internal Server Error in some cases
### Bug description
#### This query throws Internal Server Error

[Link to query 1](https://master.staging.saleor.cloud/graphql/#saleor/N4IgjiBc4K4KYCcCeACA4nALgYQPYBt84BjTAS1wDsAFBXAExlIGcAhJAZXxgHMVgAOpRQpiBIqQqUAFMQAWAQ0qU4+SCgEh6cAGYKY+TAFp5Slfk0AaFM2491mnXAWYYCOPSMAHOoxaaASn4hEREfBiZMZmkdMgRmTHUARgAGFOtmXARMdnVgFHo4knIqSABBDmxrWNV6SAAlMoA5AGkUAF8gwWFQkQ8eOGZgnt6RSgY4YdHRsnoQ6Y753valxZ6Vyg2QSxBcKBAMHHFiqVoIlnYuXhB2oA)
```graphql
query GetCollectionProductsBySlug {
  collection(channel: "default-channel", slug: "featured-products") {
    products(first: 100, sortBy: { direction:ASC, field:RANK }) {
      edges {
        node {
          id
        }
      }
    }
  }
}
```

#### throws

```json
{
  "errors": [
    {
      "message": "Internal Server Error",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "collection",
        "products"
      ],
      "extensions": {
        "exception": {
          "code": "FieldError"
        }
      }
    }
  ],
  "data": {
    "collection": {
      "products": null
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 101,
      "maximumAvailable": 50000
    }
  }
}
```

#### However, this query gives a more friendly error:
[Link to query 2](https://master.staging.saleor.cloud/graphql/#saleor/N4IgjiBc4K4KYCcCeACA4nALgYQPYBt84BjTAS1wDsAFBXAExlIGcAhJAZXxgHMVgAOpRQoADnUYsAFADMyCZpkgoAjAAY1AGhTNcCTO2XB68kuSrKAgh2za5cfPWUAlSwDkA0gF8AlPyEiInD0PHDM-sKBIpQMcBFRUWT0AQleKSJpkZleIJoguFAgGDgERKQUNBJMmGyc3DwgXkA)

```graphql
query GetCollectionProductsBySlug {
  products(first: 100, sortBy: {direction: ASC, field: RANK}) {
    edges {
      node {
        id
      }
    }
  }
}
```

```json
{
  "errors": [
    {
      "message": "Sorting by RANK is available only when using a search filter.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "products"
      ],
      "extensions": {
        "exception": {
          "code": "GraphQLError"
        }
      }
    }
  ],
  "data": {
    "products": null
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 100,
      "maximumAvailable": 50000
    }
  }
}
```
### Environment

Saleor version: staging



Discussion:

Resolving patch:
diff --git a/saleor/graphql/product/schema.py b/saleor/graphql/product/schema.py
--- a/saleor/graphql/product/schema.py
+++ b/saleor/graphql/product/schema.py
@@ -1,7 +1,4 @@
-from typing import List, Optional
-
 import graphene
-from graphql import GraphQLError
 
 from ...permission.enums import ProductPermissions
 from ...permission.utils import has_one_of_permissions
@@ -127,7 +124,6 @@
     CategorySortingInput,
     CollectionSortingInput,
     ProductOrder,
-    ProductOrderField,
     ProductTypeSortingInput,
     ProductVariantSortingInput,
 )
@@ -145,16 +141,7 @@
     ProductVariant,
     ProductVariantCountableConnection,
 )
-
-
-def search_string_in_kwargs(kwargs: dict) -> bool:
-    return bool(kwargs.get("filter", {}).get("search", "").strip()) or bool(
-        kwargs.get("search", "").strip()
-    )
-
-
-def sort_field_from_kwargs(kwargs: dict) -> Optional[List[str]]:
-    return kwargs.get("sort_by", {}).get("field") or None
+from .utils import check_for_sorting_by_rank
 
 
 class ProductQueries(graphene.ObjectType):
@@ -451,24 +438,9 @@ def resolve_product(
     @staticmethod
     @traced_resolver
     def resolve_products(_root, info: ResolveInfo, *, channel=None, **kwargs):
-        if sort_field_from_kwargs(kwargs) == ProductOrderField.RANK:
-            # sort by RANK can be used only with search filter
-            if not search_string_in_kwargs(kwargs):
-                raise GraphQLError(
-                    (
-                        "Sorting by RANK is available only when using a search filter "
-                        "or search argument."
-                    )
-                )
-        if search_string_in_kwargs(kwargs) and not sort_field_from_kwargs(kwargs):
-            # default to sorting by RANK if search is used
-            # and no explicit sorting is requested
-            product_type = info.schema.get_type("ProductOrder")
-            kwargs["sort_by"] = product_type.create_container(
-                {"direction": "-", "field": ["search_rank", "id"]}
-            )
-
+        check_for_sorting_by_rank(info, kwargs)
         search = kwargs.get("search")
+
         requestor = get_user_or_app_from_context(info.context)
         has_required_permissions = has_one_of_permissions(
             requestor, ALL_PRODUCTS_PERMISSIONS
diff --git a/saleor/graphql/product/types/collections.py b/saleor/graphql/product/types/collections.py
--- a/saleor/graphql/product/types/collections.py
+++ b/saleor/graphql/product/types/collections.py
@@ -6,6 +6,7 @@
 
 from ....permission.enums import ProductPermissions
 from ....product import models
+from ....product.search import search_products
 from ....thumbnail.utils import (
     get_image_or_proxy_url,
     get_thumbnail_format,
@@ -40,6 +41,7 @@
 )
 from ..filters import ProductFilterInput, ProductWhereInput
 from ..sorters import ProductOrder
+from ..utils import check_for_sorting_by_rank
 from .channels import CollectionChannelListing
 from .products import ProductCountableConnection
 
@@ -135,11 +137,20 @@ def _resolve_background_image(thumbnail):
     def resolve_products(
         root: ChannelContext[models.Collection], info: ResolveInfo, **kwargs
     ):
+        check_for_sorting_by_rank(info, kwargs)
+        search = kwargs.get("search")
+
         requestor = get_user_or_app_from_context(info.context)
         qs = root.node.products.visible_to_user(  # type: ignore[attr-defined] # mypy does not properly resolve the related manager # noqa: E501
             requestor, root.channel_slug
         )
-        qs = ChannelQsContext(qs=qs, channel_slug=root.channel_slug)
+
+        if search:
+            qs = ChannelQsContext(
+                qs=search_products(qs.qs, search), channel_slug=root.channel_slug
+            )
+        else:
+            qs = ChannelQsContext(qs=qs, channel_slug=root.channel_slug)
 
         kwargs["channel"] = root.channel_slug
         qs = filter_connection_queryset(qs, kwargs)
diff --git a/saleor/graphql/product/utils.py b/saleor/graphql/product/utils.py
--- a/saleor/graphql/product/utils.py
+++ b/saleor/graphql/product/utils.py
@@ -5,12 +5,14 @@
 from django.core.exceptions import ValidationError
 from django.db import DatabaseError, transaction
 from django.db.utils import IntegrityError
+from graphql import GraphQLError
 
 from ...core.tracing import traced_atomic_transaction
 from ...order import OrderStatus
 from ...order import models as order_models
 from ...warehouse.models import Stock
 from ..core.enums import ProductErrorCode
+from .sorters import ProductOrderField
 
 if TYPE_CHECKING:
     from ...product.models import ProductVariant
@@ -141,3 +143,32 @@ def update_ordered_media(ordered_media):
 
     if errors:
         raise ValidationError(errors)
+
+
+def search_string_in_kwargs(kwargs: dict) -> bool:
+    return bool(kwargs.get("filter", {}).get("search", "").strip()) or bool(
+        kwargs.get("search", "").strip()
+    )
+
+
+def sort_field_from_kwargs(kwargs: dict) -> Optional[List[str]]:
+    return kwargs.get("sort_by", {}).get("field") or None
+
+
+def check_for_sorting_by_rank(info, kwargs: dict):
+    if sort_field_from_kwargs(kwargs) == ProductOrderField.RANK:
+        # sort by RANK can be used only with search filter
+        if not search_string_in_kwargs(kwargs):
+            raise GraphQLError(
+                (
+                    "Sorting by RANK is available only when using a search filter "
+                    "or search argument."
+                )
+            )
+    if search_string_in_kwargs(kwargs) and not sort_field_from_kwargs(kwargs):
+        # default to sorting by RANK if search is used
+        # and no explicit sorting is requested
+        product_type = info.schema.get_type("ProductOrder")
+        kwargs["sort_by"] = product_type.create_container(
+            {"direction": "-", "field": ["search_rank", "id"]}
+        )


Test patch:
diff --git a/saleor/graphql/product/tests/test_collection.py b/saleor/graphql/product/tests/test_collection.py
--- a/saleor/graphql/product/tests/test_collection.py
+++ b/saleor/graphql/product/tests/test_collection.py
@@ -2127,3 +2127,32 @@ def test_pagination_for_sorting_collections_by_published_at_date(
     assert [node["node"]["slug"] for node in data["edges"]] == [
         collection.slug for collection in collections[first:]
     ]
+
+
+def test_collections_query_return_error_with_sort_by_rank_without_search(
+    staff_api_client, published_collection, product_list, channel_USD
+):
+    # given
+    for product in product_list:
+        published_collection.products.add(product)
+
+    variables = {
+        "id": graphene.Node.to_global_id("Collection", published_collection.pk),
+        "sortBy": {"direction": "DESC", "field": "RANK"},
+        "channel": channel_USD.slug,
+    }
+
+    # when
+    response = staff_api_client.post_graphql(
+        GET_SORTED_PRODUCTS_COLLECTION_QUERY, variables
+    )
+    content = get_graphql_content(response, ignore_errors=True)
+
+    # then
+    errors = content["errors"]
+    expected_message = (
+        "Sorting by RANK is available only when using a search filter "
+        "or search argument."
+    )
+    assert len(errors) == 1
+    assert errors[0]["message"] == expected_message