Skip to content

Commit

Permalink
feat: Add ValidationError code to OperationMessage (#358)
Browse files Browse the repository at this point in the history
Fix #356
  • Loading branch information
zvyn authored Sep 11, 2023
1 parent 7b5a354 commit 54aba17
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 9 deletions.
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
7 changes: 6 additions & 1 deletion strawberry_django/fields/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ class Kind(enum.Enum):
),
default=None,
)
code: Optional[str] = strawberry.field(
description="The error code, or `null` if no error code was set.",
default=None,
)

def __eq__(self, other: Self):
if not isinstance(other, OperationMessage):
Expand All @@ -187,10 +191,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

0 comments on commit 54aba17

Please sign in to comment.