pythonpython-3.xdjangographqlgraphene-python

Django Graphene when filtering on nested value, don't show parent record if no data is returned


I have a GraphQL query that will return purchase orders and the products that go with those purchase orders. I have set up two filters, the first being on the username that is associated with a purchase order, and another filter on the product name for products for the purchase orders.

If a user were to search for a particular product, I would like to only return orders that contain that product. At the moment, it still returns all purchase orders, but removes the product data that doesn't match the search criteria.

The database structure is a table that contains the purchase orders called purchase_orders and a purchase order products tables called purchase_orders_products that has a FK called order_id to the purchase order id field.

For example, in the response below, if purchaseOrdersProductsOrderId.edges is empty, I don't need to return the order data for ThisIsUsername.

Schema

# region Integration Purchase Orders
class PurchasesProducts(DjangoObjectType):
    id = graphene.ID(source='pk', required=True)

    class Meta:
        model = purchase_orders_products
        interfaces = (relay.Node,)
        filter_fields = {'product_name': ['icontains']}


class Purchases(DjangoObjectType):
    id = graphene.ID(source='pk', required=True)
    products = ArtsyConnectionField(PurchasesProducts)

    class Meta:
        model = purchase_orders
        interfaces = (relay.Node,)
        filter_fields = {'date': ['gt', 'lt', 'isnull'], 'username': ['icontains'],}
        connection_class = ArtsyConnection

    @staticmethod
    def resolve_products(self, info, **kwargs):
        return purchase_orders_products.objects.filter(order_id=self.id).order_by('product_name').all()

class PurchasesQuery(ObjectType):
    purchases = ArtsyConnectionField(Purchases)
    date_filter_list = graphene.List(graphene.List(graphene.String))

    @staticmethod
    def resolve_date_filter_list(self, info, **kwargs):
        years = purchase_orders.objects.filter(user_id=info.context.user.id).annotate(year=ExtractYear('date'), month=ExtractMonth('date'),).order_by().values_list('year', 'month').order_by('-year', '-month').distinct()
        return years

    @staticmethod
    def resolve_purchases(self, info, date_filter=None, **kwargs):
        return purchase_orders.objects.filter(user_id=info.context.user.id).all().order_by("-date")


purchasesSchema = graphene.Schema(query=PurchasesQuery)
# endregion

Query

{
  dateFilterList
  purchases(first: 15, after: "") {
    pageCursors {
      ...
    }
    edges {
      node {
        id
        ...
        products(productName_Icontains: "access") {
          edges {
            node {
              id
              productId
              productName
              productNumber
            }
          }
        }
      }
    }
  }
}

Response

{
  "data": {
    "dateFilterList": [
      ...
    ],
    "purchases": {
      "pageCursors": {
        ...
      },
      "edges": [
        {
          "node": {
            "id": "ab0d9542-480a-4a99-8f49-3474e820beb0",
            "username": "ThisIsUsername",
            ...
            "products": {
              "edges": []
            }
          }
        },
        {
          "node": {
            "id": "03e937b2-5b67-4161-90de-cdeda8dcd065",
            "username": "barry1234",
            ...
            "products": {
              "edges": [
                {
                  "node": {
                    "id": "f9945e45-59ef-42e9-9b06-988f24d1c8ed",
                    "productId": "84fae6ca-8a16-45ee-b36f-31a0ba134866",
                    "productName": "Access Denied",
                    "productNumber": "47"
                  }
                }
              ]
            }
          }
        },
        ...

Solution

  • You should define filter for product_name at Purchases instead of PurchaseProducts. The most straightforward way to do it is defining filterset_class. In my example, I have simplified a little bit queries and used DjangoFilterConnectionField because I dont know what is ArtsyConnectionField.

    from django_filters import CharFilter, FilterSet
    
    class PurchaseOrderFilter(FilterSet):
        class Meta:
            model = purchase_orders
            fields = {'date': ['gt', 'lt', 'isnull'], 'username': ['icontains'],}
    
        products__product_name = CharFilter(lookup_expr="icontains",)
    
    class PurchaseOrderProductFilter(FilterSet):
        class Meta:
            model = purchase_orders_products
            fields = {"name": ["icontains"]}
    
    class PurchasesProducts(DjangoObjectType):
        id = graphene.ID(source='pk', required=True)
    
        class Meta:
            model = purchase_orders_products
            interfaces = (graphene.relay.Node,)
            filterset_class = PurchaseOrderProductFilter
    
    
    class Purchases(DjangoObjectType):
        id = graphene.ID(source='pk', required=True)
        products = DjangoFilterConnectionField(PurchasesOrderProduct)
    
        class Meta:
            model = purchases_orders
            interfaces = (graphene.relay.Node,)
            filterset_class = PurchaseOrderFilter
    
        @staticmethod
        def resolve_products(self, info, **kwargs):
            return PurchaseOrderProduct.objects.filter(order_id=self.id).order_by('product_name').all()
    

    Now your query would look like:

    query {
        purchases(first: 15, after: "", products_ProductName: "access") {
            edges {
                node {
                    products {
                        edges {
                            node {
                                productName
                            }
                        }
                    }
                }
            }
        }
    }
    

    And the response:

    {
        "data": {
            "purchases": {
                "edges": [
                    {
                        "node": {"products": {"edges": [{"node": {"productName": "access1"}}]}}
                    }
                ]
            }
        }
    }