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
50 changes: 50 additions & 0 deletions gql/lib/src/validation/rules/executable_definitions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import "package:gql/document.dart";
import "package:gql/src/validation/validating_visitor.dart";

import "../../../ast.dart";

class NonExecutableError extends ValidationError {
const NonExecutableError({required String name, required DefinitionNode node})
: super(
message: 'The "$name" definition is not executable.',
node: node,
);

const NonExecutableError.schema({required DefinitionNode node})
: super(
message: "The schema definition is not executable.",
node: node,
);
}

/// Executable definitions
///
/// A GraphQL document is only valid for execution if all definitions are either
/// operation or fragment definitions.
///
/// See https://spec.graphql.org/draft/#sec-Executable-Definitions
class ExecutableDefinitions extends ValidatingVisitor {
@override
List<ValidationError>? visitDocumentNode(DocumentNode node) =>
node.definitions
.map((it) {
if (it is ExecutableDefinitionNode) {
return null;
} else {
if (it is TypeDefinitionNode) {
return NonExecutableError(name: it.name.value, node: it);
} else if (it is TypeExtensionNode) {
return NonExecutableError(name: it.name.value, node: it);
} else if (it is DirectiveDefinitionNode) {
return NonExecutableError(name: it.name.value, node: it);
} else if (it is SchemaDefinitionNode ||
it is SchemaExtensionNode) {
return NonExecutableError.schema(node: it);
} else {
throw StateError("Invalid node type");
}
}
})
.nonNulls
.toList();
}
113 changes: 113 additions & 0 deletions gql/lib/src/validation/rules/possible_type_extensions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import "package:gql/ast.dart";
import "package:gql/document.dart" show ValidationError;
import "package:gql/src/validation/validating_visitor.dart";

class PossibleTypeExtensionError extends ValidationError {
PossibleTypeExtensionError.nodeNotDefined(
TypeExtensionNode node,
) : super(
message:
'Cannot extend type "${node.name.value}" because it is not defined.',
node: node,
);

PossibleTypeExtensionError.incorrectType(
TypeExtensionNode node, {
required String kind,
}) : super(
message: 'Cannot extend non-${kind} type "${node.name.value}".',
node: node,
);
}

/// Possible type extension
///
/// A type extension is only valid if the type is defined and has the same kind.
class PossibleTypeExtensions extends ValidatingVisitor {
final _definedTypes = <String, TypeDefinitionNode>{};

List<ValidationError>? _recordType(TypeDefinitionNode node) {
_definedTypes[node.name.value] = node;
return null;
}

List<ValidationError>? _validateType<T extends TypeDefinitionNode>(
TypeExtensionNode node, {
required String kind,
}) {
final typeDefinition = _definedTypes[node.name.value];
if (typeDefinition == null) {
return [PossibleTypeExtensionError.nodeNotDefined(node)];
}
if (typeDefinition is! T) {
return [PossibleTypeExtensionError.incorrectType(node, kind: kind)];
}
return null;
}

// Enum
@override
List<ValidationError>? visitEnumTypeDefinitionNode(
EnumTypeDefinitionNode node) =>
_recordType(node);

@override
List<ValidationError>? visitEnumTypeExtensionNode(
EnumTypeExtensionNode node) =>
_validateType<EnumTypeDefinitionNode>(node, kind: "enum");

// Input Object
@override
List<ValidationError>? visitInputObjectTypeDefinitionNode(
InputObjectTypeDefinitionNode node) =>
_recordType(node);

@override
List<ValidationError>? visitInputObjectTypeExtensionNode(
InputObjectTypeExtensionNode node) =>
_validateType<InputObjectTypeDefinitionNode>(node, kind: "input");

// Union
@override
List<ValidationError>? visitUnionTypeDefinitionNode(
UnionTypeDefinitionNode node) =>
_recordType(node);

@override
List<ValidationError>? visitUnionTypeExtensionNode(
UnionTypeExtensionNode node) =>
_validateType<UnionTypeDefinitionNode>(node, kind: "union");

// Interface
@override
List<ValidationError>? visitInterfaceTypeDefinitionNode(
InterfaceTypeDefinitionNode node) =>
_recordType(node);

@override
List<ValidationError>? visitInterfaceTypeExtensionNode(
InterfaceTypeExtensionNode node) =>
_validateType<InterfaceTypeDefinitionNode>(node, kind: "interface");

// Object
@override
List<ValidationError>? visitObjectTypeDefinitionNode(
ObjectTypeDefinitionNode node) =>
_recordType(node);

@override
List<ValidationError>? visitObjectTypeExtensionNode(
ObjectTypeExtensionNode node) =>
_validateType<ObjectTypeDefinitionNode>(node, kind: "object");

// Scalar
@override
List<ValidationError>? visitScalarTypeDefinitionNode(
ScalarTypeDefinitionNode node) =>
_recordType(node);

@override
List<ValidationError>? visitScalarTypeExtensionNode(
ScalarTypeExtensionNode node) =>
_validateType<ScalarTypeDefinitionNode>(node, kind: "scalar");
}
83 changes: 83 additions & 0 deletions gql/lib/src/validation/rules/unique_argument_definition_names.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import "package:collection/collection.dart";
import "package:gql/ast.dart";
import "package:gql/document.dart";
import "package:gql/src/validation/validating_visitor.dart";

class DuplicateArgumentDefinitionNameError extends ValidationError {
const DuplicateArgumentDefinitionNameError(
{required String parentName, required String argumentName, Node? node})
: super(
message:
'Argument "${parentName}(${argumentName}:)" can only be defined once.',
node: node,
);
}

/// Unique argument definition names
///
/// A GraphQL Object or Interface type is only valid if all its fields have uniquely named arguments.
/// A GraphQL Directive is only valid if all its arguments are uniquely named.
class UniqueArgumentDefinitionNames extends ValidatingVisitor {
@override
List<ValidationError>? visitDirectiveDefinitionNode(
DirectiveDefinitionNode node,
) =>
_checkArgumentUniqueness(
parentName: "@${node.name.value}",
argumentNodes: node.args,
);

@override
List<ValidationError>? visitInterfaceTypeDefinitionNode(
InterfaceTypeDefinitionNode node,
) =>
_checkArgumentUniquenessPerField(name: node.name, fields: node.fields);

@override
List<ValidationError>? visitInterfaceTypeExtensionNode(
InterfaceTypeExtensionNode node,
) =>
_checkArgumentUniquenessPerField(name: node.name, fields: node.fields);

@override
List<ValidationError>? visitObjectTypeDefinitionNode(
ObjectTypeDefinitionNode node,
) =>
_checkArgumentUniquenessPerField(name: node.name, fields: node.fields);

@override
List<ValidationError>? visitObjectTypeExtensionNode(
ObjectTypeExtensionNode node,
) =>
_checkArgumentUniquenessPerField(name: node.name, fields: node.fields);

List<ValidationError> _checkArgumentUniquenessPerField({
required NameNode name,
required List<FieldDefinitionNode> fields,
}) =>
fields
.expand(
(e) => _checkArgumentUniqueness(
parentName: "${name.value}.${e.name.value}",
argumentNodes: e.args),
)
.toList();

List<ValidationError> _checkArgumentUniqueness({
required String parentName,
required List<InputValueDefinitionNode> argumentNodes,
}) =>
argumentNodes
.groupListsBy((it) => it.name.value)
.entries
.map(
(e) => e.value.length > 1
? DuplicateArgumentDefinitionNameError(
parentName: parentName,
argumentName: e.key,
)
: null,
)
.nonNulls
.toList();
}
19 changes: 16 additions & 3 deletions gql/lib/src/validation/validator.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import "package:gql/ast.dart" as ast;
import "package:gql/src/validation/rules/executable_definitions.dart";
import "package:gql/src/validation/rules/lone_schema_definition.dart";
import "package:gql/src/validation/rules/missing_fragment_definitions.dart";
import "package:gql/src/validation/rules/possible_type_extensions.dart";
import "package:gql/src/validation/rules/unique_argument_definition_names.dart";
import "package:gql/src/validation/rules/unique_argument_names.dart";
import "package:gql/src/validation/rules/unique_directive_names.dart";
import "package:gql/src/validation/rules/unique_enum_value_names.dart";
Expand Down Expand Up @@ -86,6 +89,9 @@ abstract class ValidationError {
this.message,
this.node,
});

@override
String toString() => message ?? super.toString();
}

/// Available validation rules
Expand All @@ -98,7 +104,10 @@ enum ValidationRule {
uniqueTypeNames,
uniqueInputFieldNames,
uniqueArgumentNames,
missingFragmentDefinition
missingFragmentDefinition,
possibleTypeExtensions,
uniqueArgumentDefinitionNames,
executableDefinitions,
}

ValidatingVisitor? _mapRule(ValidationRule rule) {
Expand All @@ -121,8 +130,12 @@ ValidatingVisitor? _mapRule(ValidationRule rule) {
return UniqueArgumentNames();
case ValidationRule.missingFragmentDefinition:
return const MissingFragmentDefinition();
default:
return null;
case ValidationRule.possibleTypeExtensions:
return PossibleTypeExtensions();
case ValidationRule.uniqueArgumentDefinitionNames:
return UniqueArgumentDefinitionNames();
case ValidationRule.executableDefinitions:
return ExecutableDefinitions();
}
}

Expand Down
96 changes: 96 additions & 0 deletions gql/test/validation/executable_definitions_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import "package:gql/src/validation/validator.dart";
import "package:test/test.dart";

import "./common.dart";

final validate = createValidator({
ValidationRule.executableDefinitions,
});

void main() {
group("Executable definitions", () {
test("with only operation", () {
expect(
validate(
"""
query Foo {
dog {
name
}
}
""",
),
isEmpty,
);
});

test("with operation and fragment", () {
expect(
validate(
"""
query Foo {
dog {
name
...Frag
}
}

fragment Frag on Dog {
name
}
""",
),
isEmpty,
);
});

test("with type definition", () {
expect(
validate("""
query Foo {
dog {
name
}
}

type Cow {
name: String
}

extend type Dog {
color: String
}
""").map((it) => it.toString()).toList(),
equals(
[
'The "Cow" definition is not executable.',
'The "Dog" definition is not executable.',
],
),
);
});

test("with schema definition", () {
expect(
validate("""
schema {
query: Query
}

type Query {
test: String
}

extend schema @directive
""").map((it) => it.toString()).toList(),
equals(
[
"The schema definition is not executable.",
'The "Query" definition is not executable.',
"The schema definition is not executable.",
],
),
);
});
});
}
Loading