Skip to content
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

feat: Add ValidationError code to OperationMessage #358

Merged
merged 2 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/guide/mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ type OperationMessage {
The field that caused the error, or `null` if it isn't associated with any particular field.
"""
field: String

"""The error code, or `null` if no error code was set."""
code: String
}

type Fruit {
Expand Down
6 changes: 5 additions & 1 deletion strawberry_django/fields/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ class Kind(enum.Enum):
),
default=None,
)
code: Optional[str] = strawberry.field(
description="The error code, or `null` if no error code was set.",
)

def __eq__(self, other: Self):
if not isinstance(other, OperationMessage):
Expand All @@ -187,10 +190,11 @@ def __eq__(self, other: Self):
self.kind == other.kind
and self.message == other.message
and self.field == other.field
and self.code == other.code
)

def __hash__(self):
return hash((self.__class__, self.kind, self.message, self.field))
return hash((self.__class__, self.kind, self.message, self.field, self.code))


@strawberry.type
Expand Down
7 changes: 5 additions & 2 deletions strawberry_django/mutations/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,21 @@ def _get_validation_errors(error: Exception):

if isinstance(error, ValidationError) and hasattr(error, "error_dict"):
# convert field errors
for field, field_errors in error.message_dict.items():
for field, field_errors in error.error_dict.items():
for e in field_errors:
yield OperationMessage(
kind=kind,
field=to_camel_case(field) if field != NON_FIELD_ERRORS else None,
message=e,
message=e.message % e.params if e.params else e.message,
code=getattr(e, "code", None),
)
elif isinstance(error, ValidationError) and hasattr(error, "error_list"):
# convert non-field errors
for e in error.error_list:
yield OperationMessage(
kind=kind,
message=e.message % e.params if e.params else e.message,
code=getattr(error, "code", None),
)
else:
msg = getattr(error, "msg", None)
Expand All @@ -74,6 +76,7 @@ def _get_validation_errors(error: Exception):
yield OperationMessage(
kind=kind,
message=msg,
code=getattr(error, "code", None),
)


Expand Down
24 changes: 19 additions & 5 deletions tests/projects/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,15 +402,29 @@ def create_project(
) -> ProjectType:
"""Create project documentation."""
if cost > 500:
# Field error without error code:
raise ValidationError({"cost": "Cost cannot be higher than 500"})
if cost < 0:
# Field error with error code:
raise ValidationError(
{
"cost": ValidationError(
"Cost cannot be lower than zero",
code="min_cost",
),
},
)
project = Project(
name=name,
cost=cost,
due_date=due_date,
)
project.full_clean()
project.save()

return cast(
ProjectType,
Project.objects.create(
name=name,
cost=cost,
due_date=due_date,
),
project,
)

@mutations.input_mutation(handle_django_errors=True)
Expand Down
3 changes: 3 additions & 0 deletions tests/projects/snapshots/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@ type OperationMessage {
The field that caused the error, or `null` if it isn't associated with any particular field.
"""
field: String

"""The error code, or `null` if no error code was set."""
code: String
}

enum OperationMessageKind {
Expand Down
85 changes: 84 additions & 1 deletion tests/test_input_mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,88 @@ def test_input_mutation(db, gql_client: GraphQLTestClient):
# The cost is properly set, but this user doesn't have
# permission to see it
"cost": None,
"dueDate": "2030-01-01T00:00:00",
"dueDate": "2030-01-01",
},
}


@pytest.mark.django_db(transaction=True)
def test_input_mutation_with_internal_error_code(db, gql_client: GraphQLTestClient):
query = """
mutation CreateProject ($input: CreateProjectInput!) {
createProject (input: $input) {
... on ProjectType {
name
cost
}
... on OperationInfo {
messages {
field
message
kind
code
}
}
}
}
"""
with assert_num_queries(0):
res = gql_client.query(
query,
{"input": {"name": 100 * "way to long", "cost": "10.40"}},
)
assert res.data == {
"createProject": {
"messages": [
{
"field": "name",
"kind": "VALIDATION",
"message": (
"Ensure this value has at most 255 characters (it has"
" 1100)."
),
"code": "max_length",
},
],
},
}


@pytest.mark.django_db(transaction=True)
def test_input_mutation_with_explicit_error_code(db, gql_client: GraphQLTestClient):
query = """
mutation CreateProject ($input: CreateProjectInput!) {
createProject (input: $input) {
... on ProjectType {
name
cost
}
... on OperationInfo {
messages {
field
message
kind
code
}
}
}
}
"""
with assert_num_queries(0):
res = gql_client.query(
query,
{"input": {"name": "Some Project", "cost": "-1"}},
)
assert res.data == {
"createProject": {
"messages": [
{
"field": "cost",
"kind": "VALIDATION",
"message": "Cost cannot be lower than zero",
"code": "min_cost",
},
],
},
}

Expand All @@ -62,6 +143,7 @@ def test_input_mutation_with_errors(db, gql_client: GraphQLTestClient):
field
message
kind
code
}
}
}
Expand All @@ -79,6 +161,7 @@ def test_input_mutation_with_errors(db, gql_client: GraphQLTestClient):
"field": "cost",
"kind": "VALIDATION",
"message": "Cost cannot be higher than 500",
"code": None,
},
],
},
Expand Down
Loading