Fix coercion of argument values
This commit is contained in:
parent
8f1e7eb7ce
commit
908502c66e
5 changed files with 181 additions and 30 deletions
|
@ -1,6 +1,7 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
import 'package:angel_graphql/angel_graphql.dart';
|
import 'package:angel_graphql/angel_graphql.dart';
|
||||||
import 'package:angel_serialize/angel_serialize.dart';
|
import 'package:angel_serialize/angel_serialize.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:graphql_schema/graphql_schema.dart';
|
import 'package:graphql_schema/graphql_schema.dart';
|
||||||
import 'package:graphql_server/graphql_server.dart';
|
import 'package:graphql_server/graphql_server.dart';
|
||||||
import 'package:graphql_server/mirrors.dart';
|
import 'package:graphql_server/mirrors.dart';
|
||||||
|
@ -8,6 +9,13 @@ import 'package:graphql_server/mirrors.dart';
|
||||||
main() async {
|
main() async {
|
||||||
var app = new Angel();
|
var app = new Angel();
|
||||||
var http = new AngelHttp(app);
|
var http = new AngelHttp(app);
|
||||||
|
hierarchicalLoggingEnabled = true;
|
||||||
|
app.logger = new Logger('angel_graphql')
|
||||||
|
..onRecord.listen((rec) {
|
||||||
|
print(rec);
|
||||||
|
if (rec.error != null) print(rec.error);
|
||||||
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
});
|
||||||
|
|
||||||
var todoService = app.use('api/todos', new MapService()) as Service;
|
var todoService = app.use('api/todos', new MapService()) as Service;
|
||||||
|
|
||||||
|
|
116
graphql_schema/lib/introspection.dart
Normal file
116
graphql_schema/lib/introspection.dart
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import 'package:graphql_schema/graphql_schema.dart';
|
||||||
|
|
||||||
|
// TODO: How to handle custom types???
|
||||||
|
GraphQLSchema reflectSchema(GraphQLSchema schema) {
|
||||||
|
var objectTypes = _fetchAllTypes(schema);
|
||||||
|
var typeType = _reflectSchemaTypes(schema);
|
||||||
|
|
||||||
|
var schemaType = objectType('__Schema', [
|
||||||
|
field(
|
||||||
|
'types',
|
||||||
|
type: listType(typeType),
|
||||||
|
resolve: (_, __) => objectTypes,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
var fields = <GraphQLField>[
|
||||||
|
field(
|
||||||
|
'__schema',
|
||||||
|
type: schemaType,
|
||||||
|
resolve: (_, __) => schemaType,
|
||||||
|
),
|
||||||
|
field(
|
||||||
|
'__type',
|
||||||
|
type: typeType,
|
||||||
|
arguments: [
|
||||||
|
new GraphQLFieldArgument('name', graphQLString.nonNullable())
|
||||||
|
],
|
||||||
|
resolve: (_, args) {
|
||||||
|
var name = args['name'] as String;
|
||||||
|
return objectTypes.firstWhere((t) => t.name == name,
|
||||||
|
orElse: () =>
|
||||||
|
throw new GraphQLException('No type named "$name" exists.'));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
fields.addAll(schema.query.fields);
|
||||||
|
|
||||||
|
return new GraphQLSchema(
|
||||||
|
query: objectType(schema.query.name, fields),
|
||||||
|
mutation: schema.mutation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GraphQLObjectType _reflectSchemaTypes(GraphQLSchema schema) {
|
||||||
|
var fieldType = _reflectFields();
|
||||||
|
|
||||||
|
return objectType('__Type', [
|
||||||
|
field(
|
||||||
|
'name',
|
||||||
|
type: graphQLString,
|
||||||
|
resolve: (type, _) => (type as GraphQLObjectType).name,
|
||||||
|
),
|
||||||
|
field(
|
||||||
|
'kind',
|
||||||
|
type: graphQLString,
|
||||||
|
resolve: (type, _) => 'OBJECT', // TODO: Union, interface
|
||||||
|
),
|
||||||
|
field(
|
||||||
|
'fields',
|
||||||
|
type: listType(fieldType),
|
||||||
|
resolve: (type, _) => (type as GraphQLObjectType).fields,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
GraphQLObjectType _reflectFields() {
|
||||||
|
return objectType('__Field', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GraphQLObjectType> _fetchAllTypes(GraphQLSchema schema) {
|
||||||
|
var typess = <GraphQLType>[];
|
||||||
|
typess.addAll(_fetchAllTypesFromObject(schema.query));
|
||||||
|
|
||||||
|
if (schema.mutation != null) {
|
||||||
|
typess.addAll(_fetchAllTypesFromObject(schema.mutation)
|
||||||
|
.where((t) => t is GraphQLObjectType));
|
||||||
|
}
|
||||||
|
|
||||||
|
var types = <GraphQLObjectType>[];
|
||||||
|
|
||||||
|
for (var type in typess) {
|
||||||
|
if (type is GraphQLObjectType) types.add(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.toSet().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GraphQLType> _fetchAllTypesFromObject(GraphQLObjectType objectType) {
|
||||||
|
var types = <GraphQLType>[objectType];
|
||||||
|
|
||||||
|
for (var field in objectType.fields) {
|
||||||
|
if (field.type is GraphQLObjectType) {
|
||||||
|
types.addAll(_fetchAllTypesFromObject(field.type as GraphQLObjectType));
|
||||||
|
} else {
|
||||||
|
types.addAll(_fetchAllTypesFromType(field.type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<GraphQLType> _fetchAllTypesFromType(GraphQLType type) {
|
||||||
|
var types = <GraphQLType>[];
|
||||||
|
|
||||||
|
if (type is GraphQLNonNullableType) {
|
||||||
|
types.addAll(_fetchAllTypesFromType(type.innerType));
|
||||||
|
} else if (type is GraphQLListType) {
|
||||||
|
types.addAll(_fetchAllTypesFromType(type.innerType));
|
||||||
|
} else if (type is GraphQLObjectType) {
|
||||||
|
types.addAll(_fetchAllTypesFromObject(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Enum, interface, union
|
||||||
|
return types;
|
||||||
|
}
|
|
@ -23,7 +23,7 @@ class GraphQLObjectType
|
||||||
errors.add('Unexpected field "$k" encountered in $key.');
|
errors.add('Unexpected field "$k" encountered in $key.');
|
||||||
} else {
|
} else {
|
||||||
var v = input[k];
|
var v = input[k];
|
||||||
var result = field.type.validate(k, v);
|
var result = field.type.validate(k.toString(), v);
|
||||||
|
|
||||||
if (!result.successful) {
|
if (!result.successful) {
|
||||||
errors.addAll(result.errors.map((s) => '$key: $s'));
|
errors.addAll(result.errors.map((s) => '$key: $s'));
|
||||||
|
@ -36,7 +36,7 @@ class GraphQLObjectType
|
||||||
if (errors.isNotEmpty) {
|
if (errors.isNotEmpty) {
|
||||||
return new ValidationResult._failure(errors);
|
return new ValidationResult._failure(errors);
|
||||||
} else
|
} else
|
||||||
return new ValidationResult._ok(out);
|
return new ValidationResult._ok(_foldToStringDynamic(out));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -45,7 +45,7 @@ class GraphQLObjectType
|
||||||
var field = fields.firstWhere((f) => f.name == k, orElse: () => null);
|
var field = fields.firstWhere((f) => f.name == k, orElse: () => null);
|
||||||
if (field == null)
|
if (field == null)
|
||||||
throw new UnsupportedError('Cannot serialize field "$k", which was not defined in the schema.');
|
throw new UnsupportedError('Cannot serialize field "$k", which was not defined in the schema.');
|
||||||
return out..[k] = field.serialize(value[k]);
|
return out..[k.toString()] = field.serialize(value[k]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,14 @@ class GraphQLObjectType
|
||||||
var field = fields.firstWhere((f) => f.name == k, orElse: () => null);
|
var field = fields.firstWhere((f) => f.name == k, orElse: () => null);
|
||||||
if (field == null)
|
if (field == null)
|
||||||
throw new UnsupportedError('Unexpected field "$k" encountered in map.');
|
throw new UnsupportedError('Unexpected field "$k" encountered in map.');
|
||||||
return out..[k] = field.deserialize(value[k]);
|
return out..[k.toString()] = field.deserialize(value[k]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _foldToStringDynamic(Map map) {
|
||||||
|
return map == null
|
||||||
|
? null
|
||||||
|
: map.keys.fold<Map<String, dynamic>>(
|
||||||
|
<String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);
|
||||||
|
}
|
|
@ -23,3 +23,10 @@ GraphQLSchema graphQLSchema(
|
||||||
|
|
||||||
/// A default resolver that always returns `null`.
|
/// A default resolver that always returns `null`.
|
||||||
resolveToNull(_, __) => null;
|
resolveToNull(_, __) => null;
|
||||||
|
|
||||||
|
class GraphQLException extends FormatException {
|
||||||
|
GraphQLException(String message) : super(message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'GraphQL exception: $message';
|
||||||
|
}
|
||||||
|
|
|
@ -2,15 +2,20 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:graphql_parser/graphql_parser.dart';
|
import 'package:graphql_parser/graphql_parser.dart';
|
||||||
import 'package:graphql_schema/graphql_schema.dart';
|
import 'package:graphql_schema/graphql_schema.dart';
|
||||||
|
import 'package:graphql_schema/introspection.dart';
|
||||||
|
|
||||||
class GraphQL {
|
class GraphQL {
|
||||||
final Map<String, GraphQLType> customTypes = {};
|
final Map<String, GraphQLType> customTypes = {};
|
||||||
final GraphQLSchema schema;
|
GraphQLSchema _schema;
|
||||||
|
|
||||||
GraphQL(this.schema) {
|
GraphQL(GraphQLSchema schema, {bool introspect: true}) : _schema = schema {
|
||||||
if (schema.query != null) customTypes[schema.query.name] = schema.query;
|
if (introspect) {
|
||||||
if (schema.mutation != null)
|
_schema = reflectSchema(_schema);
|
||||||
customTypes[schema.mutation.name] = schema.mutation;
|
}
|
||||||
|
|
||||||
|
if (_schema.query != null) customTypes[_schema.query.name] = _schema.query;
|
||||||
|
if (_schema.mutation != null)
|
||||||
|
customTypes[_schema.mutation.name] = _schema.mutation;
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphQLType convertType(TypeContext ctx) {
|
GraphQLType convertType(TypeContext ctx) {
|
||||||
|
@ -35,12 +40,11 @@ class GraphQL {
|
||||||
if (customTypes.containsKey(ctx.typeName.name))
|
if (customTypes.containsKey(ctx.typeName.name))
|
||||||
return customTypes[ctx.typeName.name];
|
return customTypes[ctx.typeName.name];
|
||||||
throw new ArgumentError(
|
throw new ArgumentError(
|
||||||
'Unknown GraphQL type: "${ctx.typeName.name}"\n${ctx.span.highlight()}');
|
'Unknown GraphQL type: "${ctx.typeName.name}"');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new ArgumentError(
|
throw new ArgumentError('Invalid GraphQL type: "${ctx.span.text}"');
|
||||||
'Invalid GraphQL type: "${ctx.span.text}"\n${ctx.span.highlight()}');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +56,7 @@ class GraphQL {
|
||||||
var tokens = scan(text, sourceUrl: sourceUrl);
|
var tokens = scan(text, sourceUrl: sourceUrl);
|
||||||
var parser = new Parser(tokens);
|
var parser = new Parser(tokens);
|
||||||
var document = parser.parseDocument();
|
var document = parser.parseDocument();
|
||||||
return executeRequest(schema, document,
|
return executeRequest(_schema, document,
|
||||||
operationName: operationName,
|
operationName: operationName,
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
variableValues: variableValues);
|
variableValues: variableValues);
|
||||||
|
@ -203,8 +207,8 @@ class GraphQL {
|
||||||
var value = argumentValues.firstWhere((a) => a.name == argumentName,
|
var value = argumentValues.firstWhere((a) => a.name == argumentName,
|
||||||
orElse: () => null);
|
orElse: () => null);
|
||||||
|
|
||||||
if (value != null) {
|
if (value?.valueOrVariable?.variable != null) {
|
||||||
var variableName = value.name;
|
var variableName = value.valueOrVariable.variable.name;
|
||||||
var variableValue = variableValues[variableName];
|
var variableValue = variableValues[variableName];
|
||||||
|
|
||||||
if (variableValues.containsKey(variableName)) {
|
if (variableValues.containsKey(variableName)) {
|
||||||
|
@ -214,13 +218,28 @@ class GraphQL {
|
||||||
} else if (argumentType is GraphQLNonNullableType) {
|
} else if (argumentType is GraphQLNonNullableType) {
|
||||||
throw new GraphQLException(
|
throw new GraphQLException(
|
||||||
'Missing value for argument "$argumentName".');
|
'Missing value for argument "$argumentName".');
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (value == null) {
|
||||||
if (defaultValue != null || argumentDefinition.defaultsToNull) {
|
if (defaultValue != null || argumentDefinition.defaultsToNull) {
|
||||||
coercedValues[argumentName] = defaultValue;
|
coercedValues[argumentName] = defaultValue;
|
||||||
} else if (argumentType is GraphQLNonNullableType) {
|
} else if (argumentType is GraphQLNonNullableType) {
|
||||||
throw new GraphQLException(
|
throw new GraphQLException(
|
||||||
'Missing value for argument "$argumentName".');
|
'Missing value for argument "$argumentName".');
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var validation =
|
||||||
|
argumentType.validate(fieldName, value.valueOrVariable.value.value);
|
||||||
|
|
||||||
|
if (!validation.successful) {
|
||||||
|
throw new GraphQLException(
|
||||||
|
'Coercion error for value of argument "$argumentName".');
|
||||||
|
} else {
|
||||||
|
var coercedValue = validation.value;
|
||||||
|
coercedValues[argumentName] = coercedValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,7 +265,6 @@ class GraphQL {
|
||||||
List<SelectionContext> fields,
|
List<SelectionContext> fields,
|
||||||
result,
|
result,
|
||||||
Map<String, dynamic> variableValues) async {
|
Map<String, dynamic> variableValues) async {
|
||||||
|
|
||||||
if (fieldType is GraphQLNonNullableType) {
|
if (fieldType is GraphQLNonNullableType) {
|
||||||
var innerType = fieldType.innerType;
|
var innerType = fieldType.innerType;
|
||||||
var completedResult = completeValue(
|
var completedResult = completeValue(
|
||||||
|
@ -321,8 +339,9 @@ class GraphQL {
|
||||||
GraphQLObjectType objectType,
|
GraphQLObjectType objectType,
|
||||||
SelectionSetContext selectionSet,
|
SelectionSetContext selectionSet,
|
||||||
Map<String, dynamic> variableValues,
|
Map<String, dynamic> variableValues,
|
||||||
{List visitedFragments: const []}) {
|
{List visitedFragments}) {
|
||||||
var groupedFields = <String, List<SelectionContext>>{};
|
var groupedFields = <String, List<SelectionContext>>{};
|
||||||
|
visitedFragments ??= [];
|
||||||
|
|
||||||
for (var selection in selectionSet.selections) {
|
for (var selection in selectionSet.selections) {
|
||||||
if (getDirectiveValue('skip', 'if', selection, variableValues) == true)
|
if (getDirectiveValue('skip', 'if', selection, variableValues) == true)
|
||||||
|
@ -340,9 +359,11 @@ class GraphQL {
|
||||||
if (visitedFragments.contains(fragmentSpreadName)) continue;
|
if (visitedFragments.contains(fragmentSpreadName)) continue;
|
||||||
visitedFragments.add(fragmentSpreadName);
|
visitedFragments.add(fragmentSpreadName);
|
||||||
var fragment = document.definitions
|
var fragment = document.definitions
|
||||||
.whereType<FragmentDefinitionContext>()
|
.where((d) => d is FragmentDefinitionContext)
|
||||||
.firstWhere((f) => f.name == fragmentSpreadName,
|
.firstWhere(
|
||||||
orElse: () => null);
|
(f) =>
|
||||||
|
(f as FragmentDefinitionContext).name == fragmentSpreadName,
|
||||||
|
orElse: () => null) as FragmentDefinitionContext;
|
||||||
|
|
||||||
if (fragment == null) continue;
|
if (fragment == null) continue;
|
||||||
var fragmentType = fragment.typeCondition;
|
var fragmentType = fragment.typeCondition;
|
||||||
|
@ -395,8 +416,7 @@ class GraphQL {
|
||||||
|
|
||||||
var vname = vv.variable.name;
|
var vname = vv.variable.name;
|
||||||
if (!variableValues.containsKey(vname))
|
if (!variableValues.containsKey(vname))
|
||||||
throw new GraphQLException(
|
throw new GraphQLException('Unknown variable: "$vname"');
|
||||||
'Unknown variable: "$vname"\n${vv.variable.span.highlight()}');
|
|
||||||
|
|
||||||
return variableValues[vname];
|
return variableValues[vname];
|
||||||
}
|
}
|
||||||
|
@ -416,10 +436,3 @@ class GraphQL {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GraphQLException extends FormatException {
|
|
||||||
GraphQLException(String message) : super(message);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'GraphQL exception: $message';
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue