strawberry-graphql / strawberry-django

Strawberry GraphQL Django extension
https://strawberry.rocks/docs/django
MIT License
421 stars 122 forks source link

Support multi-level nested objects create/update #603

Open EvSpirit opened 4 months ago

EvSpirit commented 4 months ago

In the latest strawberry-django version (0.47.1) single-level nested objects CUD operations work fine, however, multiple level nested objects cannot be created/updated. Consider the following types hierarchy:

class Project(models.Model):
    foo = models.CharField(max_length=255)

class Milestone(models.Model):
    bar = models.CharField(max_length=255)
    project = models.ForeignKey(Project,  on_delete=models.PROTECT)  

class Issue(models.Model):
    baz = models.CharField(max_length=255)
    milestone = models.ForeignKey(Milestone, on_delete=models.PROTECT)  

Case 1. Issue - Milestone - Project

When trying to execute the following mutation:

mutation CreateIssue ($input: IssueInput!) {
  createIssue (input: $input) {
    ... on IssueType {
      id
      milestone {
        id
        project {
          id
          name
        }
      }
    }
  }
}

input:

"input": {
    "foo": "Some Issue",
    "milestone": {
        "bar": "Some Milestone",
        "project": {
            "baz": "Some Project",
        },
    },
}

Error occurs:

ValueError: Cannot assign "UNSET": "Milestone.project" must be a "Project" instance.

That happens because of two issues:

So UNSET added to parsed data. After fixing if-clause and checking for both None and UNSET values another issue arises:

The problem is in the same function: https://github.com/strawberry-graphql/strawberry-django/blob/9025ee5a0af7c31ce8fffe2be9ce9d871b289a17/strawberry_django/mutations/resolvers.py#L102-L103

parent model (Milestone but not Project) is used, so even if full clean passes somehow (in unit tests that's the case as all models have name field), finally incorrect instance would be created. The fix seems to be quite straightforward also:

related_model = get_model_fields(model).get(k).related_model
v = create(info, related_model, v.data or {}) 

Using parent model we can extract related model and pass one to create function.

Case 2. Project - Milestone - Issue (reverse relations)

mutation UpdateProject ($input: ProjectInputPartial!) {
  updateProject (input: $input) {
    ... on ProjectType {
      id
      milestones {
        id
        issues {
          id
        }
      }
    }
  }
}

Input:

"input": {
    "id": to_base64("ProjectType", project.pk),
    "milestones": [{
        "name": "Milestone 1",
        "issues": [{
            "name": "Some Issue",
        }]
    }]
}

Error:

django.core.exceptions.FieldError: Cannot resolve keyword 'issues' into field. Choices are:

That happens because nested issues are passed directly to Django create (or get_or_create): https://github.com/strawberry-graphql/strawberry-django/blob/9025ee5a0af7c31ce8fffe2be9ce9d871b289a17/strawberry_django/mutations/resolvers.py#L623-L627

This issue seems to be related to #383 and #449 PR. #360 is also related as update_m2m function is to be updated.

I'm working on the PR to address the issues mentioned above, creating this issue to further refer one from the PR.

Upvote & Fund

Fund with Polar