Fixed validation of interface types
This commit is contained in:
parent
e4d5693ace
commit
aad1404530
10 changed files with 129 additions and 34 deletions
|
@ -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 />
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import 'package:angel_serialize/angel_serialize.dart';
|
||||
|
||||
import 'episode.dart';
|
||||
|
||||
@serializable
|
||||
abstract class Character {
|
||||
String get id;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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>{};
|
||||
|
||||
|
|
Loading…
Reference in a new issue