-
-
Notifications
You must be signed in to change notification settings - Fork 125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support multi-level nested create/update with model full_clean()
#659
base: main
Are you sure you want to change the base?
Support multi-level nested create/update with model full_clean()
#659
Conversation
Reviewer's Guide by SourceryThis PR implements multi-level nested create/update functionality for resources and ensures proper model validation through Sequence diagram for multi-level nested create/updatesequenceDiagram
actor User
participant Client
participant Server
participant Database
User->>Client: Initiate create/update request
Client->>Server: Send GraphQL mutation
Server->>Database: Validate data with full_clean()
alt Validation passes
Server->>Database: Create/update nested resources
Database-->>Server: Return success
Server-->>Client: Return success response
else Validation fails
Server-->>Client: Return validation error
end
Updated class diagram for mutation resolversclassDiagram
class MutationResolver {
+create(info, model, data, key_attr, full_clean, pre_save_hook, exclude_m2m)
+update(info, model, data, key_attr, full_clean, pre_save_hook, exclude_m2m)
+_create(info, manager, data, key_attr, full_clean, pre_save_hook, exclude_m2m)
+update_m2m(info, instance, field, value, key_attr)
}
class ProjectInputPartial {
+name
+milestones
}
class MilestoneInputPartial {
+name
+issues
}
class MilestoneIssueInputPartial {
+name
+tags
}
MutationResolver --> ProjectInputPartial
ProjectInputPartial --> MilestoneInputPartial
MilestoneInputPartial --> MilestoneIssueInputPartial
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @philipstarkey - I've reviewed your changes and they look great!
Here's what I looked at during the review
- 🟢 General issues: all looks good
- 🟢 Security: all looks good
- 🟡 Testing: 1 issue found
- 🟢 Complexity: all looks good
- 🟢 Documentation: all looks good
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
@@ -377,6 +380,479 @@ def test_input_create_with_m2m_mutation(db, gql_client: GraphQLTestClient): | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (testing): Consider adding negative test cases for nested creation
While the happy path is well tested, it would be valuable to add test cases that verify behavior when invalid data is provided at different nesting levels (e.g., invalid milestone data, invalid issue data, invalid tag data). This would help ensure proper error handling and validation at each level.
@pytest.mark.django_db(transaction=True)
@pytest.mark.parametrize("invalid_data", [
{"milestone": {"name": ""}},
{"issue": {"title": ""}},
{"tags": [{"name": " "}]}
])
def test_nested_creation_with_invalid_data(invalid_data):
with pytest.raises(ValidationError):
create_project_with_relations(invalid_data)
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #659 +/- ##
==========================================
+ Coverage 88.98% 89.09% +0.10%
==========================================
Files 41 41
Lines 3731 3759 +28
==========================================
+ Hits 3320 3349 +29
+ Misses 411 410 -1 ☔ View full report in Codecov by Sentry. |
Hi @philipstarkey , Thanks for the PR, and sorry for taking a while to take a look at it! I've been busy these past few weeks. It's looking good! Could you rebase on top of |
…e benefit of consistently calling `full_clean()` before creating related instances. This does remove the `get_or_create()` calls and instead uses `create` only. The expectation here is that `key_attr` could and should be used to indicate what field should be used as the unique identifier, and not something hard coded that could have unintended side effects when creating related instances that don't have unique constraints and expect new instances to always be created.
5230b89
to
b73c0fb
Compare
@bellini666 No worries - I saw from a comment in another PR that you were moving so I completely understand you had more important priorities! I've rebased and pushed - tests passed locally for me :) |
@philipstarkey in the case below: models: class Fruit(models.Model):
"""A tasty treat"""
name = models.CharField(
max_length=20,
)
color = models.ForeignKey(
"Color",
on_delete=models.CASCADE,
related_name="fruits",
blank=True,
null=True,
)
class Color(models.Model):
name = models.CharField(
max_length=20,
help_text="field description",
)
detailled_color = models.ForeignKey(
"DetailledColor",
on_delete=models.CASCADE,
related_name="colors",
blank=True,
null=True,
)
class DetailledColor(models.Model):
name = models.CharField(
max_length=20,
) inputs: @strawberry_django.partial(models.Fruit)
class FruitInput:
color: 'ColorInput'
...
@strawberry_django.partial(models.Color)
class ColorInput:
detailled_color: 'DetailledColorInput'
...
@strawberry_django.partial(models.DetailledColor)
class DetailledColorInput:
... does this PR covers the case of cretion of nested objects like this: |
Hmm, possibly not with the foreign key relations defined in that way? I have an example of creating nested tags in issues in milestones, in projects, but I think the foreign key's are on the other side of the relationships to your example. I suspect it's this line that would need to be updated to support your example (e.g. this should call the strawberry-django create function rather than using the manager directly): strawberry-django/strawberry_django/mutations/resolvers.py Lines 286 to 288 in b73c0fb
|
@philipstarkey I will try this modification on your pr code and then let you know if it works. |
@stygmate I'll wait for you to test what you want to test to see if this will need any extra modifications. When you give me an "ok", I'll proceed with the merge |
@bellini666 @stygmate I have extended an existing test (that pre-dates this PR) to test nested creation of foreign key relations to an even deeper depth, using a schema approximately equivalent to the one detailed above. Identified a couple of issues and fixed them in e719aa7. Has the added bonus of fixing some other edge cases with |
@philipstarkey don't work for me. mutation MyMutation {
createFruit(
data: {
name: "fraise",
color: {
name: "ruge",
detailledColor: {
name: "2"
}
}
}
) {
id
}
} result in: {
"data": null,
"errors": [
{
"message": "Cannot assign \"{'detailled_color': {'name': '2'}, 'name': 'ruge'}\": \"Fruit.color\" must be a \"Color\" instance.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createFruit"
]
}
]
} |
@stygmate Ah, I see. It looks like this is a pre-existing non-relay bug. This PR might make it more likely to be hit, but I am pretty sure it was there in some variations of single-level deep nested create/update (e.g. m2m). The root cause is that only relay input types produce @bellini666 This is definitely a bug, but I think out-of-scope for trying to fix in this PR, as it predates my changes? I think the bug is just that these lines don't get hit for non-relay types: strawberry-django/strawberry_django/mutations/resolvers.py Lines 99 to 103 in 1b49393
|
@philipstarkey I think so, that issue doesn't seem to be related to the scope of this PR. @stygmate what do you think? Do you have any other concerns? We can probably track that issue you mentioned in a separated ticket |
Description
This implements the core functionality of #604, allowing multi-level create/update of nested resources. It also ensures that
model.full_clean()
is called before creating a new resource, which ensures that internal information about constraint names doesn't leak out to the client viaIntegrityError
messages (see #398).To solve this, we allow
update_m2m
to recursively callcreate()
, ensuring that the logic for creation of records uses a consistent code path regardless of what depth in the nested data it is created at.Notes
This is not everything from #604, nor does it fix all of #603. It does fix all of #398. It seemed like #604 was getting too large to review though, so I'm hoping this approach of breaking it up into smaller chunks will work better.
Part of the reason why this approach was a bit easier was that I didn't try to preserve the change from #362. This change didn't come with any tests, has probably been superseded by the
key_attr
functionality that was added after the fix was merged, and it seems risky to assume that the default behaviour should be to reuse an existing model instance just because some fields match (as pointed out by @bellini666 previously in this comment).From the changes in this PR, I see several other subsequent bugfixes/improvements that could be made to address the remaining issues from #603 and comments raised in the previous PR #604:
key_attr
to specify more than one column to perform the lookup on (comment in Fix nested fields update #604)key_attr
and full_clean/full_clean_options to be specified individually for each type(?) of nested resource specified.project.milestones.0.name
orproject.milestones.0.issues.2.tags.3.name
could be an option?. Validation errors probably shouldn't stop other nested resources from being created, so all validation errors can be caught and returned at the end. (feature: provide path to nested field if ValidationError is raised #404)Types of Changes
Issues Fixed or Closed by This PR
Checklist
Summary by Sourcery
Implement support for multi-level nested create and update operations with model full_clean() to enhance data integrity and prevent constraint name leaks. Add tests to verify the new functionality.
New Features:
Bug Fixes:
Enhancements:
Tests: