Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 24 additions & 0 deletions modules/core/src/main/scala/schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1970,6 +1970,7 @@ object SchemaValidator {
defns: List[TypeDefinition],
typeExtnDefns: List[Ast.TypeExtension]): List[Problem] =
validateReferences(schema, defns) ++
validateRootTypes(schema) ++
validateUniqueDefns(schema) ++
validateUniqueFields(schema) ++
validateUnionMembers(schema) ++
Expand All @@ -1979,6 +1980,29 @@ object SchemaValidator {
validateTypeExtensions(defns, typeExtnDefns) ++
Directive.validateDirectivesForSchema(schema)

// https://spec.graphql.org/October2021/#sec-Root-Operation-Types
def validateRootTypes(schema: Schema): List[Problem] = {
def checkRoot(opName: String, mandatory: Boolean): List[Problem] =
schema.schemaType.field(opName).flatMap(_.nonNull.asNamed) match {
case None =>
if (mandatory) List(Problem(s"No '$opName' root operation type in schema")) else Nil
case Some(tpe) if !tpe.exists =>
List(
Problem(s"Undefined type '${tpe.name}' specified as '$opName' root operation type"))
case Some(tpe) =>
tpe.dealias match {
case _: ObjectType => Nil
case _ =>
List(Problem(
s"Type '${tpe.name}' specified as '$opName' root operation type is not an object type"))
}
}

checkRoot("query", mandatory = true) ++
checkRoot("mutation", mandatory = false) ++
checkRoot("subscription", mandatory = false)
}

def validateReferences(schema: Schema, defns: List[TypeDefinition]): List[Problem] = {
def underlyingName(tpe: Ast.Type): String =
tpe match {
Expand Down
3 changes: 3 additions & 0 deletions modules/core/src/test/scala/compiler/DirectivesSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ final class DirectivesSuite extends CatsEffectSuite {
"""|schema @foo {
| query: Query
|}
|type Query @foo {
| field(e: Enum, i: Input): Int @foo
|}
|scalar Scalar @foo
|interface Interface @foo {
| field(e: Enum, i: Input): Int @foo
Expand Down
12 changes: 10 additions & 2 deletions modules/core/src/test/scala/extensions/ExtensionsSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ final class ExtensionsSuite extends CatsEffectSuite {
val schema =
schema"""
schema {
query: Foo
query: Query
}

type Query {
foo: Int
}

scalar Foo
Expand Down Expand Up @@ -504,7 +508,11 @@ final class ExtensionsSuite extends CatsEffectSuite {
val schema = Schema(
"""
schema {
query: Foo
query: Query
}

type Query {
foo: Int
}

scalar Foo
Expand Down
138 changes: 132 additions & 6 deletions modules/core/src/test/scala/schema/SchemaSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: multiple deprecated annotations") {
val schema = Schema(
"""
type Query {
foo: Int
}

type ExampleType {
oldField: String @deprecated @deprecated
}
Expand All @@ -112,6 +116,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: deprecated annotation with unsupported argument") {
val schema = Schema(
"""
type Query {
foo: Int
}

type ExampleType {
oldField: String @deprecated(notareason: "foo bar baz")
}
Expand All @@ -130,6 +138,11 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: duplicate enum values") {
val schema = Schema(
"""
type Query {
foo: Int
}


enum Direction {
NORTH
NORTH
Expand All @@ -149,6 +162,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: object implementing unspecified interfaces") {
val schema = Schema(
"""
type Query {
foo: Int
}

type Human implements Character & Contactable {
name: String!
}
Expand All @@ -171,6 +188,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: object implementing duplicate interfaces") {
val schema = Schema(
"""
type Query {
foo: Int
}

interface Character {
id: ID!
name: String!
Expand Down Expand Up @@ -200,6 +221,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: object failing to implement interface fields") {
val schema = Schema(
"""
type Query {
foo: Int
}

interface Character {
id: ID!
name: String!
Expand Down Expand Up @@ -228,6 +253,11 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: interface failing to implement interface fields") {
val schema = Schema(
"""
type Query {
foo: Int
}


interface Character {
id: ID!
name: String!
Expand Down Expand Up @@ -256,6 +286,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: object implementing interface field with wrong type") {
val schema = Schema(
"""
type Query {
foo: Int
}

interface Character {
name: String!
}
Expand All @@ -281,6 +315,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: object implementing interface field with mismatched arguments") {
val schema = Schema(
"""
type Query {
foo: Int
}

interface Character {
name(foo: Int!): String!
}
Expand All @@ -306,6 +344,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: multiple objects failing to implement interface field") {
val schema = Schema(
"""
type Query {
foo: Int
}

interface Character {
id: ID!
name: String!
Expand Down Expand Up @@ -337,6 +379,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: object failing to implement multiple interface fields") {
val schema = Schema(
"""
type Query {
foo: Int
}

interface Character {
id: ID!
name: String!
Expand Down Expand Up @@ -368,6 +414,10 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: object correctly implements transitive interface") {
val schema = Schema(
"""
type Query {
foo: Int
}

interface Node {
id: ID!
}
Expand All @@ -386,14 +436,18 @@ final class SchemaSuite extends CatsEffectSuite {

schema match {
case Result.Success(a) =>
assertEquals(a.types.map(_.name), List("Node", "Resource", "Human"))
assertEquals(a.types.map(_.name), List("Query", "Node", "Resource", "Human"))
case unexpected => fail(s"This was unexpected: $unexpected")
}
}

test("schema validation: object failing to implement transitive interface") {
val schema = Schema(
"""
type Query {
foo: Int
}

interface Node {
id: ID!
}
Expand Down Expand Up @@ -582,8 +636,61 @@ final class SchemaSuite extends CatsEffectSuite {
assert(schema.subscriptionType.exists(_ =:= schema.ref("Subscription")))
}

test("no query type (crashes)") {
intercept[NoSuchElementException](schema"scalar Foo".queryType)
test("schema validation: no query type") {
val schema = Schema("scalar Foo")

schema match {
case Result.Failure(ps) =>
assertEquals(
ps.map(_.message),
NonEmptyChain("No 'query' root operation type in schema"))
case unexpected => fail(s"This was unexpected: $unexpected")
}
}

test("schema validation: dangling root operation type reference") {
val schema = Schema(
"""
schema {
query: Query
mutation: MutationRoot
}

type Query {
foo: Int
}
"""
)

schema match {
case Result.Failure(ps) =>
assertEquals(
ps.map(_.message),
NonEmptyChain(
"Undefined type 'MutationRoot' specified as 'mutation' root operation type"))
case unexpected => fail(s"This was unexpected: $unexpected")
}
}

test("schema validation: non-object root operation type") {
val schema = Schema(
"""
schema {
query: Foo
}

scalar Foo
"""
)

schema match {
case Result.Failure(ps) =>
assertEquals(
ps.map(_.message),
NonEmptyChain(
"Type 'Foo' specified as 'query' root operation type is not an object type"))
case unexpected => fail(s"This was unexpected: $unexpected")
}
}

test("schema validation: fields implementing interfaces can be subtypes") {
Expand Down Expand Up @@ -648,6 +755,11 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: input with non-nullable fields") {
val schema = Schema(
"""
type Query {
foo: Int
}


input ExampleInput {
foo: String!
bar: Int
Expand All @@ -657,7 +769,7 @@ final class SchemaSuite extends CatsEffectSuite {
)

schema match {
case Result.Success(s) => assertEquals(s.types.map(_.name), List("ExampleInput"))
case Result.Success(s) => assertEquals(s.types.map(_.name), List("Query", "ExampleInput"))
case unexpected => fail(s"This was unexpected: $unexpected")
}
}
Expand Down Expand Up @@ -704,6 +816,11 @@ final class SchemaSuite extends CatsEffectSuite {
test("schema validation: oneOf with all nullable fields") {
val schema = Schema(
"""
type Query {
foo: Int
}


input ExampleInput @oneOf {
foo: String
bar: Int
Expand All @@ -713,14 +830,19 @@ final class SchemaSuite extends CatsEffectSuite {
)

schema match {
case Result.Success(s) => assertEquals(s.types.map(_.name), List("ExampleInput"))
case Result.Success(s) => assertEquals(s.types.map(_.name), List("Query", "ExampleInput"))
case unexpected => fail(s"This was unexpected: $unexpected")
}
}

test("schema validation: oneOf shouldn't have required fields") {
val schema = Schema(
"""
type Query {
foo: Int
}


input ExampleInput @oneOf {
foo: String!
bar: Int
Expand All @@ -740,7 +862,11 @@ final class SchemaSuite extends CatsEffectSuite {
}

test("operations on undefined types") {
val schema = schema""
val schema = schema"""
type Query {
foo: Int
}
"""

val QuuxType = schema.uncheckedRef("Quux")
assertEquals(QuuxType.fieldInfo("quux"), None)
Expand Down
Loading