Repository: nautobot/nautobot (Python)

Issue:
GraphQL queries do not support filtering on M2M relationships
### Environment
* Nautobot version: v2.2 for the locations/prefixes issue described below, probably all versions for the M2M in general
* Python version: N/A
* Database platform, version: N/A
* Middleware(s): N/A

### Steps to Reproduce
1. Craft a GraphQL query like this where a M2M query is traversed (`location`-`prefixes` in this case). This works as expected.
```
query ($device_id: ID!) {
  device(id: $device_id) {
    name
    location {
      name
    	prefixes {
    	  prefix
        prefix_length
    	}
    }
 }
}
```

2. Add a filter to the nested item (e.g. `prefix_length` for `prefixes`). Any filter will cause the issue to manifest.
```
query ($device_id: ID!) {
  device(id: $device_id) {
    name
    location {
      name
    	prefixes (prefix_length: 16){
    	  prefix
        prefix_length
    	}
    }
 }
}
```

### Expected Behavior
The GraphQL query is executed and the filtered list of prefixes is returned.

### Observed Behavior
An error is returned
`"Unknown argument \"prefix_length\" on field \"prefixes\" of type \"LocationType\".",`

Seems adjacent to [issue #5513](https://github.com/nautobot/nautobot/issues/5513) but not necessarily directly related. Based on the discussion had with @glennmatthews, this is probably due to "a gap in the graphql generation for all M2M fields", also present in the Device/VRF relationship.



Discussion: Should be pretty similar to what we implmented in #799 for `ManyToOneRel`, see specifically `nautobot/core/graphql/schema.py`
Please target the 7/22 patch release for 2.2



Fix (patch):
diff --git a/changes/5906.fixed b/changes/5906.fixed
new file mode 100644
index 00000000000..191da1d70e7
--- /dev/null
+++ b/changes/5906.fixed
@@ -0,0 +1,1 @@
+Added support for filtering in GraphQL of objects identified by a many-to-many relation (`Location.prefixes`, `Prefix.locations`, etc.)
diff --git a/nautobot/core/graphql/generators.py b/nautobot/core/graphql/generators.py
index 266230a5d08..2da9c4d45db 100644
--- a/nautobot/core/graphql/generators.py
+++ b/nautobot/core/graphql/generators.py
@@ -55,12 +55,12 @@ def resolve_fields_w_choices(model, info, **kwargs):
 
 def generate_filter_resolver(schema_type, resolver_name, field_name):
     """
-    Generate function to resolve OneToMany filtering.
+    Generate function to resolve filtering of ManyToOne and ManyToMany related objects.
 
     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
+        field_name (str): name of ManyToOneField, ManyToManyRel, or ManyToOneRel field to filter
     """
     filterset_class = schema_type._meta.filterset_class
 
diff --git a/nautobot/core/graphql/schema.py b/nautobot/core/graphql/schema.py
index 4a0e61b06a3..9a162414aad 100644
--- a/nautobot/core/graphql/schema.py
+++ b/nautobot/core/graphql/schema.py
@@ -6,7 +6,8 @@
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.core.validators import ValidationError
-from django.db.models.fields.reverse_related import ManyToOneRel, OneToOneRel
+from django.db.models import ManyToManyField
+from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
 import graphene
 from graphene.types import generic
 
@@ -198,9 +199,11 @@ def extend_schema_type_filter(schema_type, model):
         (DjangoObjectType): The extended schema_type object
     """
     for field in model._meta.get_fields():
-        # Check attribute is a ManyToOne field
-        # OneToOneRel is a subclass of ManyToOneRel, but we don't want to treat is as a list
-        if not isinstance(field, ManyToOneRel) or isinstance(field, OneToOneRel):
+        # Check whether attribute is a ManyToOne or ManyToMany field
+        if not isinstance(field, (ManyToManyField, ManyToManyRel, ManyToOneRel)):
+            continue
+        # OneToOneRel is a subclass of ManyToOneRel, but we don't want to treat it as a list
+        if isinstance(field, OneToOneRel):
             continue
         child_schema_type = registry["graphql_types"].get(field.related_model._meta.label_lower)
         if child_schema_type: