Spec-compliant error messages

This commit is contained in:
Tobe O 2018-08-03 17:07:08 -04:00
parent 2ebc5f96da
commit ca87fbdba9
6 changed files with 124 additions and 37 deletions

View file

@ -3,6 +3,8 @@ import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_validate/server.dart';
import 'package:dart2_constant/convert.dart';
import 'package:graphql_parser/graphql_parser.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
final ContentType graphQlContentType =
@ -23,25 +25,43 @@ Map<String, dynamic> _foldToStringDynamic(Map map) {
RequestHandler graphQLHttp(GraphQL graphQl) {
return (req, res) async {
if (req.headers.contentType?.mimeType == graphQlContentType.mimeType) {
var text = utf8.decode(await req.lazyOriginalBuffer());
return {'data': await graphQl.parseAndExecute(text, sourceUrl: 'input')};
} else if (req.headers.contentType?.mimeType == 'application/json') {
if (await validate(graphQlPostBody)(req, res)) {
var text = req.body['query'] as String;
var operationName = req.body['operation_name'] as String;
var variables = req.body['variables'] as Map;
try {
if (req.headers.contentType?.mimeType == graphQlContentType.mimeType) {
var text = utf8.decode(await req.lazyOriginalBuffer());
return {
'data': await graphQl.parseAndExecute(
text,
sourceUrl: 'input',
operationName: operationName,
variableValues: _foldToStringDynamic(variables),
),
'data': await graphQl.parseAndExecute(text, sourceUrl: 'input')
};
} else if (req.headers.contentType?.mimeType == 'application/json') {
if (await validate(graphQlPostBody)(req, res)) {
var text = req.body['query'] as String;
var operationName = req.body['operation_name'] as String;
var variables = req.body['variables'] as Map;
return {
'data': await graphQl.parseAndExecute(
text,
sourceUrl: 'input',
operationName: operationName,
variableValues: _foldToStringDynamic(variables),
),
};
}
} else {
throw new AngelHttpException.badRequest();
}
} else {
throw new AngelHttpException.badRequest();
} on AngelHttpException catch (e) {
var errors = <GraphQLExceptionError>[
new GraphQLExceptionError(e.message)
];
errors
.addAll(e.errors.map((ee) => new GraphQLExceptionError(ee)).toList());
throw new GraphQLException(errors);
} on SyntaxError catch (e) {
return new GraphQLException.fromSourceSpan(e.message, e.span);
} on GraphQLException catch (e) {
return e.toJson();
} catch (e) {
return new GraphQLException.fromMessage(e.toString()).toJson();
}
};
}

View file

@ -3,6 +3,7 @@ library graphql_schema.src.schema;
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
part 'argument.dart';
@ -36,11 +37,65 @@ GraphQLSchema graphQLSchema(
/// A default resolver that always returns `null`.
resolveToNull(_, __) => null;
class GraphQLException extends FormatException {
GraphQLException(String message) : super(message);
/// An error that occurs during execution of a GraphQL query.
class GraphQLException implements Exception {
final List<GraphQLExceptionError> errors;
@override
String toString() => 'GraphQL exception: $message';
GraphQLException(this.errors);
factory GraphQLException.fromMessage(String message) {
return new GraphQLException([
new GraphQLExceptionError(message),
]);
}
factory GraphQLException.fromSourceSpan(String message, FileSpan span) {
return new GraphQLException([
new GraphQLExceptionError(
message,
locations: [
new GraphExceptionErrorLocation.fromSourceLocation(span.start),
],
),
]);
}
Map<String, List<Map<String, dynamic>>> toJson() {
return {
'errors': errors.map((e) => e.toJson()).toList(),
};
}
}
class GraphQLExceptionError {
final String message;
final List<GraphExceptionErrorLocation> locations;
GraphQLExceptionError(this.message, {this.locations: const []});
Map<String, dynamic> toJson() {
var out = <String, dynamic>{'message': message};
if (locations?.isNotEmpty == true) {
out['locations'] = locations.map((l) => l.toJson()).toList();
}
return out;
}
}
class GraphExceptionErrorLocation {
final int line;
final int column;
GraphExceptionErrorLocation(this.line, this.column);
factory GraphExceptionErrorLocation.fromSourceLocation(
SourceLocation location) {
return new GraphExceptionErrorLocation(location.line, location.column);
}
Map<String, int> toJson() {
return {'line': line, 'column': column};
}
}
/// A metadata annotation used to provide documentation to `package:graphql_server`.

View file

@ -7,5 +7,6 @@ environment:
sdk: ">=1.8.0 <3.0.0"
dependencies:
meta: ^1.0.0
source_span: ^1.0.0
dev_dependencies:
test: ^0.12.0

View file

@ -94,12 +94,12 @@ class GraphQL {
if (operationName == null) {
return ops.length == 1
? ops.first as OperationDefinitionContext
: throw new GraphQLException(
: throw new GraphQLException.fromMessage(
'This document does not define any operations.');
} else {
return ops.firstWhere(
(d) => (d as OperationDefinitionContext).name == operationName,
orElse: () => throw new GraphQLException(
orElse: () => throw new GraphQLException.fromMessage(
'Missing required operation "$operationName".'))
as OperationDefinitionContext;
}
@ -123,15 +123,21 @@ class GraphQL {
if (defaultValue != null) {
coercedValues[variableName] = defaultValue.value.value;
} else if (!variableType.isNullable) {
throw new GraphQLException(
'Missing required variable "$variableName".');
throw new GraphQLException.fromSourceSpan(
'Missing required variable "$variableName".',
variableDefinition.span);
}
} else {
var type = convertType(variableType);
var validation = type.validate(variableName, value);
if (!validation.successful) {
throw new GraphQLException(validation.errors[0]);
throw new GraphQLException(validation.errors
.map((e) => new GraphQLExceptionError(e, locations: [
new GraphExceptionErrorLocation.fromSourceLocation(
variableDefinition.span.start)
]))
.toList());
} else {
coercedValues[variableName] = type.deserialize(value);
}
@ -222,8 +228,9 @@ class GraphQL {
} else if (defaultValue != null || argumentDefinition.defaultsToNull) {
coercedValues[argumentName] = defaultValue;
} else if (argumentType is GraphQLNonNullableType) {
throw new GraphQLException(
'Missing value for argument "$argumentName".');
throw new GraphQLException.fromSourceSpan(
'Missing value for argument "$argumentName".',
value.valueOrVariable.span);
} else {
continue;
}
@ -231,8 +238,9 @@ class GraphQL {
if (defaultValue != null || argumentDefinition.defaultsToNull) {
coercedValues[argumentName] = defaultValue;
} else if (argumentType is GraphQLNonNullableType) {
throw new GraphQLException(
'Missing value for argument "$argumentName".');
throw new GraphQLException.fromSourceSpan(
'Missing value for argument "$argumentName".',
value.valueOrVariable.span);
} else {
continue;
}
@ -241,8 +249,9 @@ class GraphQL {
argumentType.validate(fieldName, value.valueOrVariable.value.value);
if (!validation.successful) {
throw new GraphQLException(
'Coercion error for value of argument "$argumentName".');
throw new GraphQLException.fromSourceSpan(
'Coercion error for value of argument "$argumentName".',
value.valueOrVariable.span);
} else {
var coercedValue = validation.value;
coercedValues[argumentName] = coercedValue;
@ -277,7 +286,7 @@ class GraphQL {
document, fieldName, innerType, fields, result, variableValues);
if (completedResult == null) {
throw new GraphQLException(
throw new GraphQLException.fromMessage(
'Null value provided for non-nullable field "$fieldName".');
} else {
return completedResult;
@ -290,7 +299,7 @@ class GraphQL {
if (fieldType is GraphQLListType) {
if (result is! Iterable) {
throw new GraphQLException(
throw new GraphQLException.fromMessage(
'Value of field "$fieldName" must be a list or iterable, got $result instead.');
}
@ -315,7 +324,7 @@ class GraphQL {
return validation.value;
}
} on TypeError {
throw new GraphQLException(
throw new GraphQLException.fromMessage(
'Value of field "$fieldName" must be ${fieldType.valueType}, got $result instead.');
}
}
@ -427,7 +436,7 @@ class GraphQL {
var vname = vv.variable.name;
if (!variableValues.containsKey(vname))
throw new GraphQLException('Unknown variable: "$vname"');
throw new GraphQLException.fromSourceSpan('Unknown variable: "$vname"', vv.span);
return variableValues[vname];
}

View file

@ -67,8 +67,8 @@ GraphQLSchema reflectSchema(GraphQLSchema schema, List<GraphQLType> allTypes) {
resolve: (_, args) {
var name = args['name'] as String;
return objectTypes.firstWhere((t) => t.name == name,
orElse: () =>
throw new GraphQLException('No type named "$name" exists.'));
orElse: () => throw new GraphQLException.fromMessage(
'No type named "$name" exists.'));
},
),
];

View file

@ -167,6 +167,8 @@ String _getDeprecationReason(List<InstanceMirror> metadata) {
}
}
}
return null;
}
String _getDescription(List<InstanceMirror> metadata) {