Skip to content

Commit

Permalink
Add unique field names validation (#1135)
Browse files Browse the repository at this point in the history
* Add unique field names validation

Fixes #1133
Also fixes #1049
as the schema's won't compile anymore, so the warning no longer happens

* Optimize duplicate?/3 check

* Add changelog entry
  • Loading branch information
maartenvanvliet authored Jan 4, 2022
1 parent 1ad9f67 commit b594d7a
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Bug Fix: [Validate field names are unique to an object/interface/input object](https://github.com/absinthe-graphql/absinthe/pull/1135)

## 1.6.6

- Feature: [Update telemetry dependency to stable ~> 1.0](https://github.com/absinthe-graphql/absinthe/pull/1097)
Expand Down
69 changes: 69 additions & 0 deletions lib/absinthe/phase/schema/validation/unique_field_names.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule Absinthe.Phase.Schema.Validation.UniqueFieldNames do
@moduledoc false

@behaviour Absinthe.Phase
alias Absinthe.Blueprint

def run(bp, _) do
bp =
bp
|> Blueprint.prewalk(&handle_schemas(&1, :name))

{:ok, bp}
end

defp handle_schemas(%Blueprint.Schema.SchemaDefinition{} = schema, key) do
schema = Blueprint.prewalk(schema, &validate_types(&1, key))
{:halt, schema}
end

defp handle_schemas(obj, _) do
obj
end

defp validate_types(%type{} = object, key)
when type in [
Blueprint.Schema.InputObjectTypeDefinition,
Blueprint.Schema.InterfaceTypeDefinition,
Blueprint.Schema.ObjectTypeDefinition
] do
fields =
for field <- object.fields do
name_counts = Enum.frequencies_by(object.fields, &Map.get(&1, key))

if duplicate?(name_counts, field, key) do
Absinthe.Phase.put_error(field, error(field, object))
else
field
end
end

%{object | fields: fields}
end

defp validate_types(type, _) do
type
end

defp duplicate?(name_counts, field, key) do
field_identifier = Map.get(field, key)
Map.get(name_counts, field_identifier, 0) > 1
end

defp error(field, object) do
%Absinthe.Phase.Error{
message: explanation(field, object),
locations: [field.__reference__.location],
phase: __MODULE__,
extra: field
}
end

def explanation(field, object) do
"""
The field #{inspect(field.name)} is not unique in type #{inspect(object.name)}.
The field must have a unique name within that Object type; no two fields may share the same name.
"""
end
end
1 change: 1 addition & 0 deletions lib/absinthe/pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ defmodule Absinthe.Pipeline do
Phase.Schema.Validation.NoInterfaceCyles,
Phase.Schema.Validation.QueryTypeMustBeObject,
Phase.Schema.Validation.NamesMustBeValid,
Phase.Schema.Validation.UniqueFieldNames,
Phase.Schema.RegisterTriggers,
Phase.Schema.MarkReferenced,
Phase.Schema.ReformatDescriptions,
Expand Down
75 changes: 75 additions & 0 deletions test/absinthe/schema/rule/unique_field_names_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
defmodule Absinthe.Schema.Rule.UniqueFieldNamesTest do
use Absinthe.Case, async: true

@duplicate_object_fields ~S(
defmodule DuplicateObjectFields do
use Absinthe.Schema
query do
end
import_sdl """
type Dog {
name: String!
name: String
}
"""
end
)

@duplicate_interface_fields ~S(
defmodule DuplicateInterfaceFields do
use Absinthe.Schema
query do
end
import_sdl """
interface Animal {
tail: Boolean
tail: Boolean
}
"""
end
)

@duplicate_input_fields ~S(
defmodule DuplicateInputFields do
use Absinthe.Schema
query do
end
import_sdl """
input AnimalInput {
species: String!
species: String!
}
"""
end
)

test "errors on non unique object field names" do
error = ~r/The field \"name\" is not unique in type \"Dog\"./

assert_raise(Absinthe.Schema.Error, error, fn ->
Code.eval_string(@duplicate_object_fields)
end)
end

test "errors on non unique interface field names" do
error = ~r/The field \"tail\" is not unique in type \"Animal\"./

assert_raise(Absinthe.Schema.Error, error, fn ->
Code.eval_string(@duplicate_interface_fields)
end)
end

test "errors on non unique input field names" do
error = ~r/The field \"species\" is not unique in type \"AnimalInput\"./

assert_raise(Absinthe.Schema.Error, error, fn ->
Code.eval_string(@duplicate_input_fields)
end)
end
end

0 comments on commit b594d7a

Please sign in to comment.