"""Library of generators for GraphQL."""

import logging

import graphene
import graphene_django_optimizer as gql_optimizer
from graphql import GraphQLError
from graphene_django import DjangoObjectType

from nautobot.core.graphql.utils import str_to_var_name, get_filtering_args_from_filterset
from nautobot.extras.choices import RelationshipSideChoices
from nautobot.extras.models import RelationshipAssociation
from nautobot.utilities.utils import get_filterset_for_model

logger = logging.getLogger("nautobot.graphql.generators")
RESOLVER_PREFIX = "resolve_"


def generate_restricted_queryset():
    """
    Generate a function to return a restricted queryset compatible with the internal permissions system.

    Note that for built-in models such as ContentType the queryset has no `restrict` method, so we have to
    fail gracefully in that case.
    """

    def get_queryset(queryset, info):
        if not hasattr(queryset, "restrict"):
            logger.debug(f"Queryset {queryset} is not restrictable")
            return queryset
        return queryset.restrict(info.context.user, "view")

    return get_queryset


def generate_null_choices_resolver(name, resolver_name):
    """
    Generate function to resolve appropriate type when a field has `null=False` (default), `blank=True`, and
    `choices` defined.

    Args:
        name (str): name of the field to resolve
        resolver_name (str): name of the resolver as declare in DjangoObjectType
    """

    def resolve_fields_w_choices(model, info, **kwargs):
        field_value = getattr(model, name)
        if field_value:
            return field_value
        return None

    resolve_fields_w_choices.__name__ = resolver_name
    return resolve_fields_w_choices


def generate_filter_resolver(schema_type, resolver_name, field_name):
    """
    Generate function to resolve OneToMany filtering.

    Args:
        schema_type (DjangoObjectType): DjangoObjectType for a given model
        resolver_name (str): name of the resolver
        field_name (str): name of OneToMany field to filter
    """
    filterset_class = schema_type._meta.filterset_class

    def resolve_filter(self, *args, **kwargs):
        if not filterset_class:
            return getattr(self, field_name).all()

        resolved_obj = filterset_class(kwargs, getattr(self, field_name).all())

        # Check result filter for errors.
        if not resolved_obj.errors:
            return resolved_obj.qs.all()

        errors = {}

        # Build error message from results
        # Error messages are collected from each filter object
        for key in resolved_obj.errors:
            errors[key] = resolved_obj.errors[key]

        # Raising this exception will send the error message in the response of the GraphQL request
        raise GraphQLError(errors)

    resolve_filter.__name__ = resolver_name
    return resolve_filter


def generate_custom_field_resolver(name, resolver_name):
    """Generate function to resolve each custom field within each DjangoObjectType.

    Args:
        name (str): name of the custom field to resolve
        resolver_name (str): name of the resolver as declare in DjangoObjectType
    """

    def resolve_custom_field(self, info, **kwargs):
        return self.cf.get(name, None)

    resolve_custom_field.__name__ = resolver_name
    return resolve_custom_field


def generate_computed_field_resolver(name, resolver_name):
    """Generate an instance method for resolving an individual computed field within a given DjangoObjectType.

    Args:
        name (str): name of the computed field to resolve
        resolver_name (str): name of the resolver as declare in DjangoObjectType
    """

    def resolve_computed_field(self, info, **kwargs):
        return self.get_computed_field(slug=name)

    resolve_computed_field.__name__ = resolver_name
    return resolve_computed_field


def generate_relationship_resolver(name, resolver_name, relationship, side, peer_model):
    """Generate function to resolve each custom relationship within each DjangoObjectType.

    Args:
        name (str): name of the custom field to resolve
        resolver_name (str): name of the resolver as declare in DjangoObjectType
        relationship (Relationship): Relationship object to generate a resolver for
        side (str): side of the relationship to use for the resolver
        peer_model (Model): Django Model of the peer of this relationship
    """

    def resolve_relationship(self, info, **kwargs):
        """Return a queryset or an object depending on the type of the relationship."""
        peer_side = RelationshipSideChoices.OPPOSITE[side]
        query_params = {"relationship": relationship}
        if not relationship.symmetric:
            # Get the objects on the other side of this relationship
            query_params[f"{side}_id"] = self.pk
            queryset_ids = gql_optimizer.query(
                RelationshipAssociation.objects.filter(**query_params).values_list(f"{peer_side}_id", flat=True), info
            )
        else:
            # Get objects that are peers for this relationship, regardless of side
            queryset_ids = list(
                gql_optimizer.query(
                    RelationshipAssociation.objects.filter(source_id=self.pk, **query_params).values_list(
                        "destination_id", flat=True
                    ),
                    info,
                )
            )
            queryset_ids += list(
                gql_optimizer.query(
                    RelationshipAssociation.objects.filter(destination_id=self.pk, **query_params).values_list(
                        "source_id", flat=True
                    ),
                    info,
                )
            )

        if relationship.has_many(peer_side):
            return gql_optimizer.query(peer_model.objects.filter(id__in=queryset_ids), info)

        return gql_optimizer.query(peer_model.objects.filter(id__in=queryset_ids).first(), info)

    resolve_relationship.__name__ = resolver_name
    return resolve_relationship


def generate_schema_type(app_name: str, model: object) -> DjangoObjectType:
    """
    Take a Django model and generate a Graphene Type class definition.

    Args:
        app_name (str): name of the application or plugin the Model is part of.
        model (object): Django Model

    Example:
        For a model with a name of "Device", the following class definition is generated:

        class DeviceType(DjangoObjectType):
            Meta:
                model = Device
                fields = ["__all__"]

        If a FilterSet exists for this model at
        '<app_name>.filters.<ModelName>FilterSet' the filterset will be stored in
        filterset_class as follows:

        class DeviceType(DjangoObjectType):
            Meta:
                model = Device
                fields = ["__all__"]
                filterset_class = DeviceFilterSet
    """

    main_attrs = {}
    meta_attrs = {"model": model, "fields": "__all__"}

    # We'll attempt to find a FilterSet corresponding to the model
    # Not all models have a FilterSet defined so the function return none if it can't find a filterset
    meta_attrs["filterset_class"] = get_filterset_for_model(model)

    main_attrs["Meta"] = type("Meta", (object,), meta_attrs)

    schema_type = type(f"{model.__name__}Type", (DjangoObjectType,), main_attrs)
    return schema_type


def generate_list_search_parameters(schema_type):
    """Generate list of query parameters for the list resolver based on a filterset."""

    search_params = {}
    if schema_type._meta.filterset_class is not None:
        search_params = get_filtering_args_from_filterset(
            schema_type._meta.filterset_class,
        )

    return search_params


def generate_single_item_resolver(schema_type, resolver_name):
    """Generate a resolver for a single element of schema_type

    Args:
        schema_type (DjangoObjectType): DjangoObjectType for a given model
        resolver_name (str): name of the resolver

    Returns:
        callable: Resolver function for a single element
    """
    model = schema_type._meta.model

    def single_resolver(self, info, **kwargs):

        obj_id = kwargs.get("id", None)
        if obj_id:
            return gql_optimizer.query(model.objects.restrict(info.context.user, "view").get(pk=obj_id), info)
        return None

    single_resolver.__name__ = resolver_name
    return single_resolver


def generate_list_resolver(schema_type, resolver_name):
    """
    Generate resolver for a list of schema_type.

    If a filterset_class is associated with the schema_type,
    the resolver will pass all arguments received to the FilterSet
    If not, it will return a restricted queryset for all objects

    Args:
        schema_type (DjangoObjectType): DjangoObjectType for a given model
        resolver_name (str): name of the resolver

    Returns:
        callable: Resolver function for list of element
    """
    model = schema_type._meta.model

    def list_resolver(self, info, **kwargs):
        filterset_class = schema_type._meta.filterset_class
        if filterset_class is not None:
            resolved_obj = filterset_class(kwargs, model.objects.restrict(info.context.user, "view").all())

            # Check result filter for errors.
            if resolved_obj.errors:
                errors = {}

                # Build error message from results
                # Error messages are collected from each filter object
                for key in resolved_obj.errors:
                    errors[key] = resolved_obj.errors[key]

                # Raising this exception will send the error message in the response of the GraphQL request
                raise GraphQLError(errors)

            return gql_optimizer.query(resolved_obj.qs.all(), info)

        return gql_optimizer.query(model.objects.restrict(info.context.user, "view").all(), info)

    list_resolver.__name__ = resolver_name
    return list_resolver


def generate_attrs_for_schema_type(schema_type):
    """Generate both attributes and resolvers for a given schema_type.

    Args:
        schema_type (DjangoObjectType): DjangoObjectType for a given model

    Returns:
        dict: Dict of attributes ready to merge into the QueryMixin class
    """
    attrs = {}
    model = schema_type._meta.model

    single_item_name = str_to_var_name(model._meta.verbose_name)
    list_name = str_to_var_name(model._meta.verbose_name_plural)

    # Define Attributes for single item and list with their search parameters
    search_params = generate_list_search_parameters(schema_type)
    attrs[single_item_name] = graphene.Field(schema_type, id=graphene.ID())
    attrs[list_name] = graphene.List(schema_type, **search_params)

    # Define Resolvers for both single item and list
    single_item_resolver_name = f"{RESOLVER_PREFIX}{single_item_name}"
    list_resolver_name = f"{RESOLVER_PREFIX}{list_name}"
    attrs[single_item_resolver_name] = generate_single_item_resolver(schema_type, single_item_resolver_name)
    attrs[list_resolver_name] = generate_list_resolver(schema_type, list_resolver_name)

    return attrs

nautobot/core/graphql/generators.py
"""Library of generators for GraphQL."""

import logging

import graphene
import graphene_django_optimizer as gql_optimizer
from graphql import GraphQLError
from graphene_django import DjangoObjectType

from nautobot.core.graphql.utils import str_to_var_name, get_filtering_args_from_filterset
from nautobot.extras.choices import RelationshipSideChoices
from nautobot.extras.models import RelationshipAssociation
from nautobot.utilities.utils import get_filterset_for_model

logger = logging.getLogger("nautobot.graphql.generators")
RESOLVER_PREFIX = "resolve_"


def generate_restricted_queryset():
    """
    Generate a function to return a restricted queryset compatible with the internal permissions system.

    Note that for built-in models such as ContentType the queryset has no `restrict` method, so we have to
    fail gracefully in that case.
    """

    def get_queryset(queryset, info):
        if not hasattr(queryset, "restrict"):
            logger.debug(f"Queryset {queryset} is not restrictable")
            return queryset
        return queryset.restrict(info.context.user, "view")

    return get_queryset


def generate_null_choices_resolver(name, resolver_name):
    """
    Generate function to resolve appropriate type when a field has `null=False` (default), `blank=True`, and
    `choices` defined.

    Args:
        name (str): name of the field to resolve
        resolver_name (str): name of the resolver as declare in DjangoObjectType
    """

    def resolve_fields_w_choices(model, info, **kwargs):
        field_value = getattr(model, name)
        if field_value:
            return field_value
        return None

    resolve_fields_w_choices.__name__ = resolver_name
    return resolve_fields_w_choices


def generate_filter_resolver(schema_type, resolver_name, field_name):
    """
    Generate function to resolve OneToMany filtering.

    Args:
        schema_type (DjangoObjectType): DjangoObjectType for a given model
        resolver_name (str): name of the resolver
        field_name (str): name of OneToMany field to filter
    """
    filterset_class = schema_type._meta.filterset_class

    def resolve_filter(self, *args, **kwargs):
        if not filterset_class:
            return getattr(self, field_name).all()

        resolved_obj = filterset_class(kwargs, getattr(self, field_name).all())

        # Check result filter for errors.
        if not resolved_obj.errors:
            return resolved_obj.qs.all()

        errors = {}

        # Build error message from results
        # Error messages are collected from each filter object
        for key in resolved_obj.errors:
            errors[key] = resolved_obj.errors[key]

        # Raising this exception will send the error message in the response of the GraphQL request
        raise GraphQLError(errors)

    resolve_filter.__name__ = resolver_name
    return resolve_filter


def generate_custom_field_resolver(name, resolver_name):
    """Generate function to resolve each custom field within each DjangoObjectType.

    Args:
        name (str): name of the custom field to resolve
        resolver_name (str): name of the resolver as declare in DjangoObjectType
    """

    def resolve_custom_field(self, info, **kwargs):
        return self.cf.get(name, None)

    resolve_custom_field.__name__ = resolver_name
    return resolve_custom_field


def generate_computed_field_resolver(name, resolver_name):
    """Generate an instance method for resolving an individual computed field within a given DjangoObjectType.

    Args:
        name (str): name of the computed field to resolve
        resolver_name (str): name of the resolver as declare in DjangoObjectType
    """

    def resolve_computed_field(self, info, **kwargs):
        return self.get_computed_field(slug=name)

    resolve_computed_field.__name__ = resolver_name
    return resolve_computed_field


def generate_relationship_resolver(name, resolver_name, relationship, side, peer_model):
    """Generate function to resolve each custom relationship within each DjangoObjectType.

    Args:
        name (str): name of the custom field to resolve
        resolver_name (str): name of the resolver as declare in DjangoObjectType
        relationship (Relationship): Relationship object to generate a resolver for
        side (str): side of the relationship to use for the resolver
        peer_model (Model): Django Model of the peer of this relationship
    """

    def resolve_relationship(self, info, **kwargs):
        """Return a queryset or an object depending on the type of the relationship."""
        peer_side = RelationshipSideChoices.OPPOSITE[side]
        query_params = {"relationship": relationship}
        if not relationship.symmetric:
            # Get the objects on the other side of this relationship
            query_params[f"{side}_id"] = self.pk
            queryset_ids = gql_optimizer.query(
                RelationshipAssociation.objects.filter(**query_params).values_list(f"{peer_side}_id", flat=True), info
            )
        else:
            # Get objects that are peers for this relationship, regardless of side
            queryset_ids = list(
                gql_optimizer.query(
                    RelationshipAssociation.objects.filter(source_id=self.pk, **query_params).values_list(
                        "destination_id", flat=True
                    ),
                    info,
                )
            )
            queryset_ids += list(
                gql_optimizer.query(
                    RelationshipAssociation.objects.filter(destination_id=self.pk, **query_params).values_list(
                        "source_id", flat=True
                    ),
                    info,
                )
            )

        if relationship.has_many(peer_side):
            return gql_optimizer.query(peer_model.objects.filter(id__in=queryset_ids), info)

        return gql_optimizer.query(peer_model.objects.filter(id__in=queryset_ids).first(), info)

    resolve_relationship.__name__ = resolver_name
    return resolve_relationship


def generate_schema_type(app_name: str, model: object) -> DjangoObjectType:
    """
    Take a Django model and generate a Graphene Type class definition.

    Args:
        app_name (str): name of the application or plugin the Model is part of.
        model (object): Django Model

    Example:
        For a model with a name of "Device", the following class definition is generated:

        class DeviceType(DjangoObjectType):
            Meta:
                model = Device
                fields = ["__all__"]

        If a FilterSet exists for this model at
        '<app_name>.filters.<ModelName>FilterSet' the filterset will be stored in
        filterset_class as follows:

        class DeviceType(DjangoObjectType):
            Meta:
                model = Device
                fields = ["__all__"]
                filterset_class = DeviceFilterSet
    """

    main_attrs = {}
    meta_attrs = {"model": model, "fields": "__all__"}

    # We'll attempt to find a FilterSet corresponding to the model
    # Not all models have a FilterSet defined so the function return none if it can't find a filterset
    meta_attrs["filterset_class"] = get_filterset_for_model(model)

    main_attrs["Meta"] = type("Meta", (object,), meta_attrs)

    schema_type = type(f"{model.__name__}Type", (DjangoObjectType,), main_attrs)
    return schema_type


def generate_list_search_parameters(schema_type):
    """Generate list of query parameters for the list resolver based on a filterset."""

    search_params = {}
    if schema_type._meta.filterset_class is not None:
        search_params = get_filtering_args_from_filterset(
            schema_type._meta.filterset_class,
        )

    return search_params


def generate_single_item_resolver(schema_type, resolver_name):
    """Generate a resolver for a single element of schema_type

    Args:
        schema_type (DjangoObjectType): DjangoObjectType for a given model
        resolver_name (str): name of the resolver

    Returns:
        callable: Resolver function for a single element
    """
    model = schema_type._meta.model

    def single_resolver(self, info, **kwargs):

        obj_id = kwargs.get("id", None)
        if obj_id:
            return gql_optimizer.query(
                model.objects.restrict(info.context.user, "view").filter(pk=obj_id), info
            ).first()
        return None

    single_resolver.__name__ = resolver_name
    return single_resolver


def generate_list_resolver(schema_type, resolver_name):
    """
    Generate resolver for a list of schema_type.

    If a filterset_class is associated with the schema_type,
    the resolver will pass all arguments received to the FilterSet
    If not, it will return a restricted queryset for all objects

    Args:
        schema_type (DjangoObjectType): DjangoObjectType for a given model
        resolver_name (str): name of the resolver

    Returns:
        callable: Resolver function for list of element
    """
    model = schema_type._meta.model

    def list_resolver(self, info, **kwargs):
        filterset_class = schema_type._meta.filterset_class
        if filterset_class is not None:
            resolved_obj = filterset_class(kwargs, model.objects.restrict(info.context.user, "view").all())

            # Check result filter for errors.
            if resolved_obj.errors:
                errors = {}

                # Build error message from results
                # Error messages are collected from each filter object
                for key in resolved_obj.errors:
                    errors[key] = resolved_obj.errors[key]

                # Raising this exception will send the error message in the response of the GraphQL request
                raise GraphQLError(errors)

            return gql_optimizer.query(resolved_obj.qs.all(), info)

        return gql_optimizer.query(model.objects.restrict(info.context.user, "view").all(), info)

    list_resolver.__name__ = resolver_name
    return list_resolver


def generate_attrs_for_schema_type(schema_type):
    """Generate both attributes and resolvers for a given schema_type.

    Args:
        schema_type (DjangoObjectType): DjangoObjectType for a given model

    Returns:
        dict: Dict of attributes ready to merge into the QueryMixin class
    """
    attrs = {}
    model = schema_type._meta.model

    single_item_name = str_to_var_name(model._meta.verbose_name)
    list_name = str_to_var_name(model._meta.verbose_name_plural)

    # Define Attributes for single item and list with their search parameters
    search_params = generate_list_search_parameters(schema_type)
    attrs[single_item_name] = graphene.Field(schema_type, id=graphene.ID())
    attrs[list_name] = graphene.List(schema_type, **search_params)

    # Define Resolvers for both single item and list
    single_item_resolver_name = f"{RESOLVER_PREFIX}{single_item_name}"
    list_resolver_name = f"{RESOLVER_PREFIX}{list_name}"
    attrs[single_item_resolver_name] = generate_single_item_resolver(schema_type, single_item_resolver_name)
    attrs[list_resolver_name] = generate_list_resolver(schema_type, list_resolver_name)

    return attrs

nautobot/core/graphql/generators.py
diff --git a/nautobot/core/graphql/generators.py b/nautobot/core/graphql/generators.py
index 6da4d45fd8f..0e6515918fa 100644
--- a/nautobot/core/graphql/generators.py
+++ b/nautobot/core/graphql/generators.py
@@ -235,7 +235,9 @@ def single_resolver(self, info, **kwargs):
 
         obj_id = kwargs.get("id", None)
         if obj_id:
-            return gql_optimizer.query(model.objects.restrict(info.context.user, "view").get(pk=obj_id), info)
+            return gql_optimizer.query(
+                model.objects.restrict(info.context.user, "view").filter(pk=obj_id), info
+            ).first()
         return None
 
     single_resolver.__name__ = resolver_name
diff --git a/nautobot/core/tests/test_graphql.py b/nautobot/core/tests/test_graphql.py
index 4a0db3b2fdd..3fa6614ba59 100644
--- a/nautobot/core/tests/test_graphql.py
+++ b/nautobot/core/tests/test_graphql.py
@@ -488,6 +488,14 @@ def setUp(self):
         }
         """
 
+        self.get_rack_query = """
+        query ($id: ID!) {
+            rack (id: $id) {
+                name
+            }
+        }
+        """
+
     def test_graphql_api_token_with_perm(self):
         """Validate that users can query based on their permissions."""
         # First user
@@ -579,6 +587,14 @@ def test_graphql_query_variables(self):
         names = [item["name"] for item in response.data["data"]["racks"]]
         self.assertEqual(names, ["Rack 2-1", "Rack 2-2"])
 
+    def test_graphql_single_object_query(self):
+        """Validate graphql query for a single object as opposed to a set of objects also works."""
+        payload = {"query": self.get_rack_query, "variables": {"id": Rack.objects.first().pk}}
+        response = self.clients[2].post(self.api_url, payload, format="json")
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertIsInstance(response.data["data"]["rack"], dict)
+        self.assertEqual(response.data["data"]["rack"]["name"], Rack.objects.first().name)
+
     def test_graphql_query_multi_level(self):
         """Validate request with multiple levels return the proper information, following the permissions."""
         response = self.clients[0].post(self.api_url, {"query": self.get_sites_racks_query}, format="json")
@@ -805,24 +821,36 @@ def test_query_config_context_and_custom_field_data(self):
                 config_context
                 _custom_field_data
             }
+            device (id: "%s") {
+                name
+                config_context
+                _custom_field_data
+            }
         }
-        """
+        """ % (
+            self.device1.id,
+        )
 
         expected_data = {"a": 123, "b": 456, "c": 777}
 
         result = self.execute_query(query)
 
         self.assertIsInstance(result.data["devices"], list)
+        self.assertIsInstance(result.data["device"], dict)
+
         device_names = [item["name"] for item in result.data["devices"]]
         self.assertEqual(sorted(device_names), ["Device 1", "Device 2", "Device 3"])
+        self.assertEqual(result.data["device"]["name"], "Device 1")
 
         config_contexts = [item["config_context"] for item in result.data["devices"]]
         self.assertIsInstance(config_contexts[0], dict)
         self.assertDictEqual(config_contexts[0], expected_data)
+        self.assertEqual(result.data["device"]["config_context"], expected_data)
 
         custom_field_data = [item["_custom_field_data"] for item in result.data["devices"]]
         self.assertIsInstance(custom_field_data[0], dict)
         self.assertEqual(custom_field_data[0], {})
+        self.assertEqual(result.data["device"]["_custom_field_data"], {})
 
     @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
     def test_query_device_role_filter(self):
diff --git a/nautobot/docs/release-notes/version-1.2.md b/nautobot/docs/release-notes/version-1.2.md
index d5681a78020..556dd451e85 100644
--- a/nautobot/docs/release-notes/version-1.2.md
+++ b/nautobot/docs/release-notes/version-1.2.md
@@ -132,6 +132,7 @@ Just as with the UI, the `slug` can still always be explicitly set if desired.
 
 - [#1078](https://github.com/nautobot/nautobot/issues/1078) - Fixed missing support for filtering several models by their custom fields and/or created/updated stamps.
 - [#1093](https://github.com/nautobot/nautobot/pull/1093) - Improved REST API performance by adding caching of serializer "opt-in fields".
+- [#1112](https://github.com/nautobot/nautobot/issues/1112) - Fixed broken single-object GraphQL query endpoints.
 - [#1116](https://github.com/nautobot/nautobot/issues/1116) - Fixed UnboundLocalError when using device NAPALM integration
 
 ## v1.2.0b1 (2021-11-19)

GraphQL single-object endpoints broken
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version: 3.7
* Nautobot version: 1.2.0-beta1

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Build Nautobot instance of 1.2.0-beta1.
2. Add some sample devices and associated data.
3. Attempt to use the GraphiQL endpoint with a simple query such as:

```
query ($device_id: ID!) {
  device(id: $device_id) {
    hostname: name
  }
}
```

along with one of your device's UUID as the query.

<!-- What did you expect to happen? -->
### Expected Behavior
Expect to see the device's hostname returned.

<!-- What happened instead? -->
### Observed Behavior
Get error with following:

```
{
  "errors": [
    {
      "message": "'Device' object has no attribute 'only'",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "device"
      ]
    }
  ],
  "data": {
    "device": null
  }
}
```

A larger query returns this:

```
{
  "errors": [
    {
      "message": "'Device' object has no attribute 'select_related'",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "device"
      ]
    }
  ],
  "data": {
    "device": null
  }
}
```

This appears to be related to optimizations that were done in the core.
