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"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="server.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true"> <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="filePath" value="$PROJECT_DIR$/example_star_wars/bin/server.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/example_star_wars" /> <option name="workingDirectory" value="$PROJECT_DIR$/example_star_wars" />
<method /> <method />

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
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';
@ -14,19 +15,22 @@ Future configureServer(Angel app) async {
var droidService = mountService<Droid>(app, '/api/droids'); var droidService = mountService<Droid>(app, '/api/droids');
var humansService = mountService<Human>(app, '/api/humans'); var humansService = mountService<Human>(app, '/api/humans');
var starshipService = mountService<Starship>(app, '/api/starships'); var starshipService = mountService<Starship>(app, '/api/starships');
var rnd = new Random();
// Create the GraphQL schema. // Create the GraphQL schema.
// This code uses dart:mirrors to easily create GraphQL types from Dart PODO's. // 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 episodeType = convertDartType(Episode);
var humanType = convertDartType(Human); var humanType = convertDartClass(Human);
var starshipType = convertDartType(Starship); var starshipType = convertDartType(Starship);
var heroType = new GraphQLUnionType('Hero', [droidType, humanType]);
// Create the query type. // Create the query type.
// //
// Use the `resolveViaServiceIndex` helper to load data directly from an // Use the `resolveViaServiceIndex` helper to load data directly from an
// Angel service. // Angel service.
var queryType = objectType('StarWarsQuery', var queryType = objectType(
'StarWarsQuery',
description: 'A long time ago, in a galaxy far, far away...', description: 'A long time ago, in a galaxy far, far away...',
fields: [ fields: [
field( field(
@ -44,7 +48,29 @@ Future configureServer(Angel app) async {
type: listType(starshipType.nonNullable()), type: listType(starshipType.nonNullable()),
resolve: resolveViaServiceIndex(starshipService), 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. // Finally, create the schema.
var schema = graphQLSchema(query: queryType); var schema = graphQLSchema(query: queryType);
@ -60,9 +86,28 @@ Future configureServer(Angel app) async {
if (!app.isProduction) { if (!app.isProduction) {
app.get('/graphiql', graphiQL()); 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( Service mountService<T extends Model>(Angel app, String path) =>
path, app.use(path, new TypedService(new MapService())) as Service;
new TypedService(new MapService(
autoIdAndDateFields: false, autoSnakeCaseNames: false))) as Service;

View file

@ -35,6 +35,22 @@ class GraphQLObjectType
if (input is! Map) if (input is! Map)
return new ValidationResult._failure(['Expected "$key" to be a 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 = {}; var out = {};
List<String> errors = []; List<String> errors = [];
@ -51,7 +67,8 @@ 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) {
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 { } else {
var v = input[k]; var v = input[k];
var result = field.type.validate(k.toString(), v); var result = field.type.validate(k.toString(), v);

View file

@ -181,7 +181,8 @@ class GraphQL {
var mutationType = schema.mutation; var mutationType = schema.mutation;
if (mutationType == null) { 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; var selectionSet = mutation.selectionSet;
@ -364,7 +365,7 @@ class GraphQL {
if (fieldType is GraphQLObjectType && !fieldType.isInterface) { if (fieldType is GraphQLObjectType && !fieldType.isInterface) {
objectType = fieldType; objectType = fieldType;
} else { } else {
objectType = resolveAbstractType(fieldType, result); objectType = resolveAbstractType(fieldName, fieldType, result);
} }
var subSelectionSet = mergeSelectionSets(fields); var subSelectionSet = mergeSelectionSets(fields);
@ -375,29 +376,46 @@ class GraphQL {
throw new UnsupportedError('Unsupported type: $fieldType'); throw new UnsupportedError('Unsupported type: $fieldType');
} }
GraphQLObjectType resolveAbstractType(GraphQLType type, result) { GraphQLObjectType resolveAbstractType(String fieldName, GraphQLType type, result) {
List<GraphQLObjectType> possibleTypes; List<GraphQLObjectType> possibleTypes;
if (type is GraphQLObjectType) { if (type is GraphQLObjectType) {
if (type.isInterface) {
possibleTypes = type.possibleTypes; possibleTypes = type.possibleTypes;
} else {
return type;
}
} else if (type is GraphQLUnionType) { } else if (type is GraphQLUnionType) {
possibleTypes = type.possibleTypes; possibleTypes = type.possibleTypes;
} else { } else {
throw new ArgumentError(); throw new ArgumentError();
} }
var errors = <GraphQLExceptionError>[];
for (var t in possibleTypes) { for (var t in possibleTypes) {
try { try {
var validation = var validation =
t.validate('@root', foldToStringDynamic(result as Map)); t.validate(fieldName, foldToStringDynamic(result as Map));
if (validation.successful) { if (validation.successful) {
return t; 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) { SelectionSetContext mergeSelectionSets(List<SelectionContext> fields) {

View file

@ -462,6 +462,8 @@ Iterable<GraphQLType> _fetchAllTypesFromType(GraphQLType type) {
} else if (type is GraphQLEnumType) { } else if (type is GraphQLEnumType) {
types.add(type); types.add(type);
} else if (type is GraphQLUnionType) { } else if (type is GraphQLUnionType) {
types.add(type);
for (var t in type.possibleTypes) { for (var t in type.possibleTypes) {
types.addAll(_fetchAllTypesFromType(t)); 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 = final Map<Type, GraphQLType> _cache =
<Type, GraphQLType>{}; <Type, GraphQLType>{};