From 908502c66ec46f6d98c472133f200253a5327ac9 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Thu, 2 Aug 2018 14:33:50 -0400 Subject: [PATCH] Fix coercion of argument values --- angel_graphql/example/main.dart | 8 ++ graphql_schema/lib/introspection.dart | 116 ++++++++++++++++++++++++ graphql_schema/lib/src/object_type.dart | 15 ++- graphql_schema/lib/src/schema.dart | 7 ++ graphql_server/lib/graphql_server.dart | 65 +++++++------ 5 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 graphql_schema/lib/introspection.dart diff --git a/angel_graphql/example/main.dart b/angel_graphql/example/main.dart index 8afe4f41..d16c6dd1 100644 --- a/angel_graphql/example/main.dart +++ b/angel_graphql/example/main.dart @@ -1,6 +1,7 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:angel_graphql/angel_graphql.dart'; import 'package:angel_serialize/angel_serialize.dart'; +import 'package:logging/logging.dart'; import 'package:graphql_schema/graphql_schema.dart'; import 'package:graphql_server/graphql_server.dart'; import 'package:graphql_server/mirrors.dart'; @@ -8,6 +9,13 @@ import 'package:graphql_server/mirrors.dart'; main() async { var app = new Angel(); 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; diff --git a/graphql_schema/lib/introspection.dart b/graphql_schema/lib/introspection.dart new file mode 100644 index 00000000..5903253d --- /dev/null +++ b/graphql_schema/lib/introspection.dart @@ -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 = [ + 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 _fetchAllTypes(GraphQLSchema schema) { + var typess = []; + typess.addAll(_fetchAllTypesFromObject(schema.query)); + + if (schema.mutation != null) { + typess.addAll(_fetchAllTypesFromObject(schema.mutation) + .where((t) => t is GraphQLObjectType)); + } + + var types = []; + + for (var type in typess) { + if (type is GraphQLObjectType) types.add(type); + } + + return types.toSet().toList(); +} + +List _fetchAllTypesFromObject(GraphQLObjectType objectType) { + var types = [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 _fetchAllTypesFromType(GraphQLType type) { + var types = []; + + 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; +} diff --git a/graphql_schema/lib/src/object_type.dart b/graphql_schema/lib/src/object_type.dart index 46d775a2..8f98a72e 100644 --- a/graphql_schema/lib/src/object_type.dart +++ b/graphql_schema/lib/src/object_type.dart @@ -23,7 +23,7 @@ class GraphQLObjectType errors.add('Unexpected field "$k" encountered in $key.'); } else { var v = input[k]; - var result = field.type.validate(k, v); + var result = field.type.validate(k.toString(), v); if (!result.successful) { errors.addAll(result.errors.map((s) => '$key: $s')); @@ -36,7 +36,7 @@ class GraphQLObjectType if (errors.isNotEmpty) { return new ValidationResult._failure(errors); } else - return new ValidationResult._ok(out); + return new ValidationResult._ok(_foldToStringDynamic(out)); } @override @@ -45,7 +45,7 @@ class GraphQLObjectType var field = fields.firstWhere((f) => f.name == k, orElse: () => null); if (field == null) 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); if (field == null) 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 _foldToStringDynamic(Map map) { + return map == null + ? null + : map.keys.fold>( + {}, (out, k) => out..[k.toString()] = map[k]); +} \ No newline at end of file diff --git a/graphql_schema/lib/src/schema.dart b/graphql_schema/lib/src/schema.dart index cfbb90d2..ff9bed3f 100644 --- a/graphql_schema/lib/src/schema.dart +++ b/graphql_schema/lib/src/schema.dart @@ -23,3 +23,10 @@ GraphQLSchema graphQLSchema( /// A default resolver that always returns `null`. resolveToNull(_, __) => null; + +class GraphQLException extends FormatException { + GraphQLException(String message) : super(message); + + @override + String toString() => 'GraphQL exception: $message'; +} diff --git a/graphql_server/lib/graphql_server.dart b/graphql_server/lib/graphql_server.dart index daed470f..dcd4bf53 100644 --- a/graphql_server/lib/graphql_server.dart +++ b/graphql_server/lib/graphql_server.dart @@ -2,15 +2,20 @@ import 'dart:async'; import 'package:graphql_parser/graphql_parser.dart'; import 'package:graphql_schema/graphql_schema.dart'; +import 'package:graphql_schema/introspection.dart'; class GraphQL { final Map customTypes = {}; - final GraphQLSchema schema; + GraphQLSchema _schema; - GraphQL(this.schema) { - if (schema.query != null) customTypes[schema.query.name] = schema.query; - if (schema.mutation != null) - customTypes[schema.mutation.name] = schema.mutation; + GraphQL(GraphQLSchema schema, {bool introspect: true}) : _schema = schema { + if (introspect) { + _schema = reflectSchema(_schema); + } + + if (_schema.query != null) customTypes[_schema.query.name] = _schema.query; + if (_schema.mutation != null) + customTypes[_schema.mutation.name] = _schema.mutation; } GraphQLType convertType(TypeContext ctx) { @@ -35,12 +40,11 @@ class GraphQL { if (customTypes.containsKey(ctx.typeName.name)) return customTypes[ctx.typeName.name]; throw new ArgumentError( - 'Unknown GraphQL type: "${ctx.typeName.name}"\n${ctx.span.highlight()}'); + 'Unknown GraphQL type: "${ctx.typeName.name}"'); break; } } else { - throw new ArgumentError( - 'Invalid GraphQL type: "${ctx.span.text}"\n${ctx.span.highlight()}'); + throw new ArgumentError('Invalid GraphQL type: "${ctx.span.text}"'); } } @@ -52,7 +56,7 @@ class GraphQL { var tokens = scan(text, sourceUrl: sourceUrl); var parser = new Parser(tokens); var document = parser.parseDocument(); - return executeRequest(schema, document, + return executeRequest(_schema, document, operationName: operationName, initialValue: initialValue, variableValues: variableValues); @@ -203,8 +207,8 @@ class GraphQL { var value = argumentValues.firstWhere((a) => a.name == argumentName, orElse: () => null); - if (value != null) { - var variableName = value.name; + if (value?.valueOrVariable?.variable != null) { + var variableName = value.valueOrVariable.variable.name; var variableValue = variableValues[variableName]; if (variableValues.containsKey(variableName)) { @@ -214,13 +218,28 @@ class GraphQL { } else if (argumentType is GraphQLNonNullableType) { throw new GraphQLException( 'Missing value for argument "$argumentName".'); + } else { + continue; } - } else { + } else if (value == null) { if (defaultValue != null || argumentDefinition.defaultsToNull) { coercedValues[argumentName] = defaultValue; } else if (argumentType is GraphQLNonNullableType) { throw new GraphQLException( '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 fields, result, Map variableValues) async { - if (fieldType is GraphQLNonNullableType) { var innerType = fieldType.innerType; var completedResult = completeValue( @@ -321,8 +339,9 @@ class GraphQL { GraphQLObjectType objectType, SelectionSetContext selectionSet, Map variableValues, - {List visitedFragments: const []}) { + {List visitedFragments}) { var groupedFields = >{}; + visitedFragments ??= []; for (var selection in selectionSet.selections) { if (getDirectiveValue('skip', 'if', selection, variableValues) == true) @@ -340,9 +359,11 @@ class GraphQL { if (visitedFragments.contains(fragmentSpreadName)) continue; visitedFragments.add(fragmentSpreadName); var fragment = document.definitions - .whereType() - .firstWhere((f) => f.name == fragmentSpreadName, - orElse: () => null); + .where((d) => d is FragmentDefinitionContext) + .firstWhere( + (f) => + (f as FragmentDefinitionContext).name == fragmentSpreadName, + orElse: () => null) as FragmentDefinitionContext; if (fragment == null) continue; var fragmentType = fragment.typeCondition; @@ -395,8 +416,7 @@ class GraphQL { var vname = vv.variable.name; if (!variableValues.containsKey(vname)) - throw new GraphQLException( - 'Unknown variable: "$vname"\n${vv.variable.span.highlight()}'); + throw new GraphQLException('Unknown variable: "$vname"'); return variableValues[vname]; } @@ -416,10 +436,3 @@ class GraphQL { return false; } } - -class GraphQLException extends FormatException { - GraphQLException(String message) : super(message); - - @override - String toString() => 'GraphQL exception: $message'; -}