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">
|
<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 />
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,37 +15,62 @@ 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(
|
||||||
description: 'A long time ago, in a galaxy far, far away...',
|
'StarWarsQuery',
|
||||||
fields: [
|
description: 'A long time ago, in a galaxy far, far away...',
|
||||||
field(
|
fields: [
|
||||||
'droids',
|
field(
|
||||||
type: listType(droidType.nonNullable()),
|
'droids',
|
||||||
resolve: resolveViaServiceIndex(droidService),
|
type: listType(droidType.nonNullable()),
|
||||||
),
|
resolve: resolveViaServiceIndex(droidService),
|
||||||
field(
|
),
|
||||||
'humans',
|
field(
|
||||||
type: listType(humanType.nonNullable()),
|
'humans',
|
||||||
resolve: resolveViaServiceIndex(humansService),
|
type: listType(humanType.nonNullable()),
|
||||||
),
|
resolve: resolveViaServiceIndex(humansService),
|
||||||
field(
|
),
|
||||||
'starships',
|
field(
|
||||||
type: listType(starshipType.nonNullable()),
|
'starships',
|
||||||
resolve: resolveViaServiceIndex(starshipService),
|
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.
|
// 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;
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
possibleTypes = type.possibleTypes;
|
if (type.isInterface) {
|
||||||
|
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) {
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>{};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue