Fixed validation of interface types

This commit is contained in:
Tobe O 2018-08-04 11:01:49 -04:00
parent e4d5693ace
commit aad1404530
10 changed files with 129 additions and 34 deletions

View file

@ -1,6 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="server.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="VMOptions" value="--observe" />
<option name="filePath" value="$PROJECT_DIR$/example_star_wars/bin/server.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/example_star_wars" />
<method />

View file

@ -1,5 +1,8 @@
import 'package:angel_serialize/angel_serialize.dart';
import 'episode.dart';
@serializable
abstract class Character {
String get id;

View file

@ -1,8 +1,10 @@
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'character.dart';
import 'episode.dart';
@serializable
class Droid extends Model implements Character {
String name;
List<Character> friends;

View file

@ -1,9 +1,11 @@
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'character.dart';
import 'episode.dart';
import 'starship.dart';
@serializable
class Human extends Model implements Character {
String name;
List<Character> friends;

View file

@ -1,5 +1,7 @@
import 'package:angel_model/angel_model.dart';
import 'package:angel_serialize/angel_serialize.dart';
@serializable
class Starship extends Model {
String name;
int length;

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_graphql/angel_graphql.dart';
@ -14,19 +15,22 @@ Future configureServer(Angel app) async {
var droidService = mountService<Droid>(app, '/api/droids');
var humansService = mountService<Human>(app, '/api/humans');
var starshipService = mountService<Starship>(app, '/api/starships');
var rnd = new Random();
// Create the GraphQL schema.
// This code uses dart:mirrors to easily create GraphQL types from Dart PODO's.
var droidType = convertDartType(Droid);
var droidType = convertDartClass(Droid);
var episodeType = convertDartType(Episode);
var humanType = convertDartType(Human);
var humanType = convertDartClass(Human);
var starshipType = convertDartType(Starship);
var heroType = new GraphQLUnionType('Hero', [droidType, humanType]);
// Create the query type.
//
// Use the `resolveViaServiceIndex` helper to load data directly from an
// Angel service.
var queryType = objectType('StarWarsQuery',
var queryType = objectType(
'StarWarsQuery',
description: 'A long time ago, in a galaxy far, far away...',
fields: [
field(
@ -44,7 +48,29 @@ Future configureServer(Angel app) async {
type: listType(starshipType.nonNullable()),
resolve: resolveViaServiceIndex(starshipService),
),
]);
field(
'hero',
type: heroType,
resolve: (_, args) async {
var allHeroes = [];
var allDroids = await droidService.index() as Iterable;
var allHumans = await humansService.index() as Iterable;
allHeroes..addAll(allDroids)..addAll(allHumans);
// Ignore the annoying cast here, hopefully Dart 2 fixes cases like this
allHeroes = allHeroes
.where((m) =>
!args.containsKey('ep') ||
(m['appears_in'].contains(args['ep']) as bool))
.toList();
return allHeroes.isEmpty
? null
: allHeroes[rnd.nextInt(allHeroes.length)];
},
),
],
);
// Finally, create the schema.
var schema = graphQLSchema(query: queryType);
@ -60,9 +86,28 @@ Future configureServer(Angel app) async {
if (!app.isProduction) {
app.get('/graphiql', graphiQL());
}
// Seed the database.
var leia = await humansService.create({
'name': 'Leia Organa',
'appears_in': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'total_credits': 520,
});
var hanSolo = await humansService.create({
'name': 'Han Solo',
'appears_in': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'total_credits': 23,
'friends': [leia],
});
var luke = await humansService.create({
'name': 'Luke Skywalker',
'appears_in': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'total_credits': 682,
'friends': [leia, hanSolo],
});
}
Service mountService<T extends Model>(Angel app, String path) => app.use(
path,
new TypedService(new MapService(
autoIdAndDateFields: false, autoSnakeCaseNames: false))) as Service;
Service mountService<T extends Model>(Angel app, String path) =>
app.use(path, new TypedService(new MapService())) as Service;

View file

@ -35,6 +35,22 @@ class GraphQLObjectType
if (input is! Map)
return new ValidationResult._failure(['Expected "$key" to be a Map.']);
if (isInterface) {
List<String> errors = [];
for (var type in possibleTypes) {
var result = type.validate(key, input);
if (result.successful) {
return result;
} else {
errors.addAll(result.errors);
}
}
return new ValidationResult<Map<String, dynamic>>._failure(errors);
}
var out = {};
List<String> errors = [];
@ -51,7 +67,8 @@ class GraphQLObjectType
var field = fields.firstWhere((f) => f.name == k, orElse: () => null);
if (field == null) {
errors.add('Unexpected field "$k" encountered in $key.');
errors.add(
'Unexpected field "$k" encountered in $key. Accepted values on type $name: ${fields.map((f) => f.name).toList()}');
} else {
var v = input[k];
var result = field.type.validate(k.toString(), v);

View file

@ -181,7 +181,8 @@ class GraphQL {
var mutationType = schema.mutation;
if (mutationType == null) {
throw new GraphQLException.fromMessage('The schema does not define a mutation type.');
throw new GraphQLException.fromMessage(
'The schema does not define a mutation type.');
}
var selectionSet = mutation.selectionSet;
@ -364,7 +365,7 @@ class GraphQL {
if (fieldType is GraphQLObjectType && !fieldType.isInterface) {
objectType = fieldType;
} else {
objectType = resolveAbstractType(fieldType, result);
objectType = resolveAbstractType(fieldName, fieldType, result);
}
var subSelectionSet = mergeSelectionSets(fields);
@ -375,29 +376,46 @@ class GraphQL {
throw new UnsupportedError('Unsupported type: $fieldType');
}
GraphQLObjectType resolveAbstractType(GraphQLType type, result) {
GraphQLObjectType resolveAbstractType(String fieldName, GraphQLType type, result) {
List<GraphQLObjectType> possibleTypes;
if (type is GraphQLObjectType) {
if (type.isInterface) {
possibleTypes = type.possibleTypes;
} else {
return type;
}
} else if (type is GraphQLUnionType) {
possibleTypes = type.possibleTypes;
} else {
throw new ArgumentError();
}
var errors = <GraphQLExceptionError>[];
for (var t in possibleTypes) {
try {
var validation =
t.validate('@root', foldToStringDynamic(result as Map));
t.validate(fieldName, foldToStringDynamic(result as Map));
if (validation.successful) {
return t;
}
} catch (_) {}
errors
.addAll(validation.errors.map((m) => new GraphQLExceptionError(m)));
} catch (_, st) {
print('Um... $_');
print(st);
}
}
throw new StateError('Cannot convert value $result to type $type.');
errors.insert(
0,
new GraphQLExceptionError(
'Cannot convert value $result to type $type.'));
throw new GraphQLException(errors);
}
SelectionSetContext mergeSelectionSets(List<SelectionContext> fields) {

View file

@ -462,6 +462,8 @@ Iterable<GraphQLType> _fetchAllTypesFromType(GraphQLType type) {
} else if (type is GraphQLEnumType) {
types.add(type);
} else if (type is GraphQLUnionType) {
types.add(type);
for (var t in type.possibleTypes) {
types.addAll(_fetchAllTypesFromType(t));
}

View file

@ -16,6 +16,11 @@ GraphQLType convertDartType(Type type, [List<Type> typeArguments]) {
}
}
/// Shorthand for [convertDartType], for when you know the result will be an object type.
GraphQLObjectType convertDartClass(Type type, [List<Type> typeArguments]) {
return convertDartType(type, typeArguments) as GraphQLObjectType;
}
final Map<Type, GraphQLType> _cache =
<Type, GraphQLType>{};