Spec-compliant error messages
This commit is contained in:
parent
2ebc5f96da
commit
ca87fbdba9
6 changed files with 124 additions and 37 deletions
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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.'));
|
||||
},
|
||||
),
|
||||
];
|
||||
|
|
|
@ -167,6 +167,8 @@ String _getDeprecationReason(List<InstanceMirror> metadata) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String _getDescription(List<InstanceMirror> metadata) {
|
||||
|
|
Loading…
Reference in a new issue