GraphQL input objects

This commit is contained in:
Tobe O 2018-08-04 15:18:53 -04:00
parent 7e0eaa387d
commit 2256b2afdb
49 changed files with 1028 additions and 353 deletions

View file

@ -1,5 +1,6 @@
<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

@ -25,15 +25,15 @@ main() async {
fields: [
field(
'todos',
type: listType(convertDartType(Todo).nonNullable()),
listType(convertDartType(Todo).nonNullable()),
resolve: resolveViaServiceIndex(todoService),
),
field(
'todo',
type: convertDartType(Todo),
convertDartType(Todo),
resolve: resolveViaServiceRead(todoService),
arguments: [
new GraphQLFieldArgument('id', graphQLId.nonNullable()),
inputs: [
new GraphQLFieldInput('id', graphQLId.nonNullable()),
],
),
],
@ -45,14 +45,14 @@ main() async {
fields: [
field(
'create',
type: graphQLString,
graphQLString,
),
],
);
var schema = graphQLSchema(
query: queryType,
mutation: mutationType,
queryType: queryType,
mutationType: mutationType,
);
app.all('/graphql', graphQLHttp(new GraphQL(schema)));

View file

@ -1,3 +1,3 @@
export 'src/graphiql.dart';
export 'src/graphql_http.dart';
export 'src/resolvers.dart';
export 'src/resolvers.dart';

View file

@ -38,7 +38,7 @@ String renderGraphiql({String graphqlEndpoint: '/graphql'}) {
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.js"></script>
<script>
window.onload = function() {
function graphQLFetcher(graphQLParams) {

View file

@ -1,4 +1,5 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_validate/server.dart';
import 'package:dart2_constant/convert.dart';
@ -81,7 +82,14 @@ RequestHandler graphQLHttp(GraphQL graphQL) {
return new GraphQLException.fromSourceSpan(e.message, e.span);
} on GraphQLException catch (e) {
return e.toJson();
} catch (e) {
} catch (e, st) {
if (req.app?.logger != null) {
req.app.logger.severe(
'An error occurred while processing GraphQL query at ${req.uri}.',
e,
st);
}
return new GraphQLException.fromMessage(e.toString()).toJson();
}
};

View file

@ -3,11 +3,10 @@ import 'package:graphql_schema/graphql_schema.dart';
/// A GraphQL resolver that `index`es an Angel service.
///
/// The arguments passed to the resolver will be forwarded to service, and the
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceIndex<Value, Serialized>(Service service,
{String idField: 'id'}) {
resolveViaServiceIndex<Value, Serialized>(Service service) {
return (_, arguments) async {
var params = {'query': arguments, 'provider': Providers.graphql};
@ -15,9 +14,24 @@ GraphQLFieldResolver<Value, Serialized>
};
}
/// A GraphQL resolver that calls `findOne` on an Angel service.
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceFindOne<Value, Serialized>(Service service) {
return (_, arguments) async {
var params = {'query': arguments, 'provider': Providers.graphql};
return await service.findOne(params) as Value;
};
}
/// A GraphQL resolver that `read`s a single value from an Angel service.
///
/// The arguments passed to the resolver will be forwarded to service, and the
/// This resolver should be used on a field with at least the following inputs:
/// * `id`: a [graphQLId] or [graphQLString]
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceRead<Value, Serialized>(Service service,
@ -28,3 +42,59 @@ GraphQLFieldResolver<Value, Serialized>
return await service.read(id, params) as Value;
};
}
/// A GraphQL resolver that `modifies` a single value from an Angel service.
///
/// This resolver should be used on a field with at least the following inputs:
/// * `id`: a [graphQLId] or [graphQLString]
/// * `data`: a [GraphQLObjectType] corresponding to the format of `data` to be passed to `modify`
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceModify<Value, Serialized>(Service service,
{String idField: 'id'}) {
return (_, arguments) async {
var params = {'provider': Providers.graphql};
var id = arguments.remove(idField);
return await service.modify(id, arguments['data'], params) as Value;
};
}
/// A GraphQL resolver that `update`s a single value from an Angel service.
///
/// This resolver should be used on a field with at least the following inputs:
/// * `id`: a [graphQLId] or [graphQLString]
/// * `data`: a [GraphQLObjectType] corresponding to the format of `data` to be passed to `update`
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
///
/// Keep in mind that `update` **overwrites** existing contents.
/// To avoid this, use [resolveViaServiceModify] instead.
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceUpdate<Value, Serialized>(Service service,
{String idField: 'id'}) {
return (_, arguments) async {
var params = {'provider': Providers.graphql};
var id = arguments.remove(idField);
return await service.update(id, arguments['data'], params) as Value;
};
}
/// A GraphQL resolver that `remove`s a single value from an Angel service.
///
/// This resolver should be used on a field with at least the following inputs:
/// * `id`: a [graphQLId] or [graphQLString]
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceRemove<Value, Serialized>(Service service,
{String idField: 'id'}) {
return (_, arguments) async {
var params = {'query': arguments, 'provider': Providers.graphql};
var id = arguments.remove(idField);
return await service.remove(id, params) as Value;
};
}

View file

@ -2,4 +2,4 @@ export 'character.dart';
export 'droid.dart';
export 'episode.dart';
export 'human.dart';
export 'starship.dart';
export 'starship.dart';

View file

@ -32,4 +32,4 @@ AnsiCode chooseLogColor(Level level) {
return cyan;
else if (level == Level.FINER || level == Level.FINEST) return lightGray;
return resetAll;
}
}

View file

@ -23,6 +23,8 @@ Future configureServer(Angel app) async {
var episodeType = convertDartType(Episode);
var humanType = convertDartClass(Human);
var starshipType = convertDartType(Starship);
// A Hero can be either a Droid or Human; create a union type that represents this.
var heroType = new GraphQLUnionType('Hero', [droidType, humanType]);
// Create the query type.
@ -35,48 +37,62 @@ Future configureServer(Angel app) async {
fields: [
field(
'droids',
type: listType(droidType.nonNullable()),
listType(droidType.nonNullable()),
description: 'All droids in the known galaxy.',
resolve: resolveViaServiceIndex(droidService),
),
field(
'humans',
type: listType(humanType.nonNullable()),
listType(humanType.nonNullable()),
description: 'All humans in the known galaxy.',
resolve: resolveViaServiceIndex(humansService),
),
field(
'starships',
type: listType(starshipType.nonNullable()),
listType(starshipType.nonNullable()),
description: 'All starships in the known galaxy.',
resolve: resolveViaServiceIndex(starshipService),
),
field(
'hero',
type: heroType,
arguments: [
new GraphQLFieldArgument('ep', episodeType),
heroType,
description:
'Finds a random hero within the known galaxy, whether a Droid or Human.',
inputs: [
new GraphQLFieldInput('ep', episodeType),
],
resolve: (_, args) async {
var allHeroes = [];
var allDroids = await droidService.index() as Iterable;
var allHumans = await humansService.index() as Iterable;
allHeroes..addAll(allDroids)..addAll(allHumans);
resolve: randomHeroResolver(droidService, humansService, rnd),
),
],
);
// 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();
// Convert our object types to input objects, so that they can be passed to
// mutations.
var humanChangesType = humanType.asInputObject('HumanChanges');
return allHeroes.isEmpty
? null
: allHeroes[rnd.nextInt(allHeroes.length)];
},
// Create the mutation type.
var mutationType = objectType(
'StarWarsMutation',
fields: [
// We'll use the `modify_human` mutation to modify a human in the database.
field(
'modify_human',
humanType.nonNullable(),
description: 'Modifies a human in the database.',
inputs: [
new GraphQLFieldInput('id', graphQLId.nonNullable()),
new GraphQLFieldInput('data', humanChangesType.nonNullable()),
],
resolve: resolveViaServiceModify(humansService),
),
],
);
// Finally, create the schema.
var schema = graphQLSchema(query: queryType);
var schema = graphQLSchema(
queryType: queryType,
mutationType: mutationType,
);
// Next, create a GraphQL object, which will be passed to `graphQLHttp`, and
// used to mount a spec-compliant GraphQL endpoint on the server.
@ -110,7 +126,8 @@ Future configureServer(Angel app) async {
'friends': [leia, lando],
});
var luke = await humansService.create({
// Luke, of course.
await humansService.create({
'name': 'Luke Skywalker',
'appears_in': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'total_credits': 682,
@ -118,5 +135,24 @@ Future configureServer(Angel app) async {
});
}
GraphQLFieldResolver randomHeroResolver(
Service droidService, Service humansService, Random rnd) {
return (_, 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)];
};
}
Service mountService<T extends Model>(Angel app, String path) =>
app.use(path, new TypedService(new MapService())) as Service;

View file

@ -47,4 +47,4 @@ class MapQuerier extends GraphQLVisitor {
// Set output field...
result[alias] = data[realName];
}
}
}

View file

@ -2,11 +2,11 @@ import 'package:source_span/source_span.dart';
import '../token.dart';
import 'value.dart';
class ArrayValueContext extends ValueContext {
class ListValueContext extends ValueContext {
final Token LBRACKET, RBRACKET;
final List<ValueContext> values = [];
ArrayValueContext(this.LBRACKET, this.RBRACKET);
ListValueContext(this.LBRACKET, this.RBRACKET);
@override
FileSpan get span {

View file

@ -14,6 +14,7 @@ export 'fragment_definition.dart';
export 'fragment_spread.dart';
export 'inline_fragment.dart';
export 'list_type.dart';
export 'misc_value.dart';
export 'node.dart';
export 'number_value.dart';
export 'operation_definition.dart';

View file

@ -2,7 +2,7 @@ import '../token.dart';
import 'package:source_span/source_span.dart';
import 'value.dart';
class BooleanValueContext extends ValueContext {
class BooleanValueContext extends ValueContext<bool> {
bool _valueCache;
final Token BOOLEAN;
@ -13,7 +13,7 @@ class BooleanValueContext extends ValueContext {
bool get booleanValue => _valueCache ??= BOOLEAN.text == 'true';
@override
get value => booleanValue;
bool get value => booleanValue;
@override
FileSpan get span => BOOLEAN.span;

View file

@ -0,0 +1,71 @@
import 'package:source_span/source_span.dart';
import '../token.dart';
import 'node.dart';
import 'value.dart';
class NullValueContext extends ValueContext<Null> {
final Token NULL;
NullValueContext(this.NULL);
@override
FileSpan get span => NULL.span;
@override
Null get value => null;
}
class EnumValueContext extends ValueContext<String> {
final Token NAME;
EnumValueContext(this.NAME);
@override
FileSpan get span => NAME.span;
@override
String get value => NAME.span.text;
}
class ObjectValueContext extends ValueContext<Map<String, dynamic>> {
final Token LBRACE;
final List<ObjectFieldContext> fields;
final Token RBRACE;
ObjectValueContext(this.LBRACE, this.fields, this.RBRACE);
@override
FileSpan get span {
var left = LBRACE.span;
for (var field in fields) {
left = left.expand(field.span);
}
return left.expand(RBRACE.span);
}
@override
Map<String, dynamic> get value {
if (fields.isEmpty) {
return <String, dynamic>{};
} else {
return fields.fold<Map<String, dynamic>>(<String, dynamic>{},
(map, field) {
return map..[field.NAME.text] = field.value.value;
});
}
}
}
class ObjectFieldContext extends Node {
final Token NAME;
final Token COLON;
final ValueContext value;
ObjectFieldContext(this.NAME, this.COLON, this.value);
@override
FileSpan get span => NAME.span.expand(COLON.span).expand(value.span);
}

View file

@ -3,7 +3,7 @@ import 'package:source_span/source_span.dart';
import '../token.dart';
import 'value.dart';
class NumberValueContext extends ValueContext {
class NumberValueContext extends ValueContext<num> {
final Token NUMBER;
NumberValueContext(this.NUMBER);
@ -21,7 +21,7 @@ class NumberValueContext extends ValueContext {
}
@override
get value => numberValue;
num get value => numberValue;
@override
FileSpan get span => NUMBER.span;

View file

@ -1,19 +1,28 @@
import 'package:charcode/charcode.dart';
import 'package:source_span/source_span.dart';
import '../syntax_error.dart';
import '../token.dart';
import 'value.dart';
class StringValueContext extends ValueContext {
final Token STRING;
final bool isBlockString;
StringValueContext(this.STRING);
StringValueContext(this.STRING, {this.isBlockString: false});
@override
FileSpan get span => STRING.span;
String get stringValue {
var text = STRING.text.substring(1, STRING.text.length - 1);
String text;
if (!isBlockString) {
text = STRING.text.substring(1, STRING.text.length - 1);
} else {
text = STRING.text.substring(3, STRING.text.length - 3).trim();
}
var codeUnits = text.codeUnits;
var buf = new StringBuffer();
@ -55,8 +64,7 @@ class StringValueContext extends ValueContext {
buf.writeCharCode(next);
}
} else
throw new SyntaxError(
'Unexpected "\\" in string literal.', span);
throw new SyntaxError('Unexpected "\\" in string literal.', span);
} else {
buf.writeCharCode(ch);
}

View file

@ -1,5 +1,5 @@
import 'node.dart';
abstract class ValueContext extends Node {
get value;
abstract class ValueContext<T> extends Node {
T get value;
}

View file

@ -9,6 +9,7 @@ final RegExp _boolean = new RegExp(r'true|false');
final RegExp _number = new RegExp(r'-?[0-9]+(\.[0-9]+)?(E|e(\+|-)?[0-9]+)?');
final RegExp _string = new RegExp(
r'"((\\(["\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^"\\]))*"');
final RegExp _blockString = new RegExp(r'"""(([^"])|(\\"""))*"""');
final RegExp _name = new RegExp(r'[_A-Za-z][_0-9A-Za-z]*');
final Map<Pattern, TokenType> _patterns = {
@ -29,9 +30,11 @@ final Map<Pattern, TokenType> _patterns = {
'mutation': TokenType.MUTATION,
'on': TokenType.ON,
'query': TokenType.QUERY,
'null': TokenType.NULL,
_boolean: TokenType.BOOLEAN,
_number: TokenType.NUMBER,
_string: TokenType.STRING,
_blockString: TokenType.BLOCK_STRING,
_name: TokenType.NAME
};
@ -53,8 +56,7 @@ List<Token> scan(String text, {sourceUrl}) {
if (potential.isEmpty) {
var ch = new String.fromCharCode(scanner.readChar());
throw new SyntaxError(
"Unexpected token '$ch'.", scanner.emptySpan);
throw new SyntaxError("Unexpected token '$ch'.", scanner.emptySpan);
} else {
// Choose longest token
potential.sort((a, b) => b.text.length.compareTo(a.text.length));

View file

@ -10,6 +10,7 @@ class Parser {
int _index = -1;
final List<Token> tokens;
final List<SyntaxError> errors = <SyntaxError>[];
Parser(this.tokens);
@ -34,6 +35,12 @@ class Parser {
Token maybe(TokenType type) => next(type) ? current : null;
void eatCommas() {
while (next(TokenType.COMMA)) {
continue;
}
}
DocumentContext parseDocument() {
List<DefinitionContext> defs = [];
DefinitionContext def = parseDefinition();
@ -64,10 +71,12 @@ class Parser {
return new OperationDefinitionContext(
TYPE, NAME, variables, selectionSet)
..directives.addAll(dirs);
else
throw new SyntaxError(
'Expected selection set in fragment definition.',
NAME?.span ?? TYPE.span);
else {
errors.add(new SyntaxError(
'Missing selection set in fragment definition.',
NAME?.span ?? TYPE.span));
return null;
}
} else
return null;
}
@ -88,22 +97,30 @@ class Parser {
return new FragmentDefinitionContext(
FRAGMENT, NAME, ON, typeCondition, selectionSet)
..directives.addAll(dirs);
else
throw new SyntaxError(
else {
errors.add(new SyntaxError(
'Expected selection set in fragment definition.',
typeCondition.span);
} else
throw new SyntaxError(
typeCondition.span));
return null;
}
} else {
errors.add(new SyntaxError(
'Expected type condition after "on" in fragment definition.',
ON.span);
} else
throw new SyntaxError(
ON.span));
return null;
}
} else {
errors.add(new SyntaxError(
'Expected "on" after name "${NAME.text}" in fragment definition.',
NAME.span);
} else
throw new SyntaxError(
NAME.span));
return null;
}
} else {
errors.add(new SyntaxError(
'Expected name after "fragment" in fragment definition.',
FRAGMENT.span);
FRAGMENT.span));
return null;
}
} else
return null;
}
@ -136,16 +153,25 @@ class Parser {
return new InlineFragmentContext(
ELLIPSIS, ON, typeCondition, selectionSet)
..directives.addAll(directives);
} else
throw new SyntaxError('Expected selection set in inline fragment.',
directives.isEmpty ? typeCondition.span : directives.last.span);
} else
throw new SyntaxError(
'Expected type condition after "on" in inline fragment.',
ON.span);
} else
throw new SyntaxError(
'Expected "on" after "..." in inline fragment.', ELLIPSIS.span);
} else {
errors.add(new SyntaxError(
'Missing selection set in inline fragment.',
directives.isEmpty
? typeCondition.span
: directives.last.span));
return null;
}
} else {
errors.add(new SyntaxError(
'Missing type condition after "on" in inline fragment.',
ON.span));
return null;
}
} else {
errors.add(new SyntaxError(
'Missing "on" after "..." in inline fragment.', ELLIPSIS.span));
return null;
}
} else
return null;
}
@ -158,16 +184,19 @@ class Parser {
while (selection != null) {
selections.add(selection);
next(TokenType.COMMA);
eatCommas();
selection = parseSelection();
}
eatCommas();
if (next(TokenType.RBRACE))
return new SelectionSetContext(LBRACE, current)
..selections.addAll(selections);
else
throw new SyntaxError('Expected "}" after selection set.',
selections.isEmpty ? LBRACE.span : selections.last.span);
else {
errors.add(new SyntaxError('Missing "}" after selection set.',
selections.isEmpty ? LBRACE.span : selections.last.span));
return null;
}
} else
return null;
}
@ -192,7 +221,7 @@ class Parser {
var directives = parseDirectives();
var selectionSet = parseSelectionSet();
return new FieldContext(fieldName, selectionSet)
..arguments.addAll(args)
..arguments.addAll(args ?? <ArgumentContext>[])
..directives.addAll(directives);
} else
return null;
@ -206,9 +235,11 @@ class Parser {
if (next(TokenType.NAME))
return new FieldNameContext(
null, new AliasContext(NAME1, COLON, current));
else
throw new SyntaxError(
'Expected name after colon in alias.', COLON.span);
else {
errors.add(new SyntaxError(
'Missing name after colon in alias.', COLON.span));
return null;
}
} else
return new FieldNameContext(NAME1);
} else
@ -223,16 +254,18 @@ class Parser {
while (def != null) {
defs.add(def);
maybe(TokenType.COMMA);
eatCommas();
def = parseVariableDefinition();
}
if (next(TokenType.RPAREN))
return new VariableDefinitionsContext(LPAREN, current)
..variableDefinitions.addAll(defs);
else
throw new SyntaxError(
'Expected ")" after variable definitions.', LPAREN.span);
else {
errors.add(new SyntaxError(
'Missing ")" after variable definitions.', LPAREN.span));
return null;
}
} else
return null;
}
@ -247,12 +280,16 @@ class Parser {
var defaultValue = parseDefaultValue();
return new VariableDefinitionContext(
variable, COLON, type, defaultValue);
} else
throw new SyntaxError(
'Expected type in variable definition.', COLON.span);
} else
throw new SyntaxError(
'Expected ":" in variable definition.', variable.span);
} else {
errors.add(new SyntaxError(
'Missing type in variable definition.', COLON.span));
return null;
}
} else {
errors.add(new SyntaxError(
'Missing ":" in variable definition.', variable.span));
return null;
}
} else
return null;
}
@ -277,10 +314,14 @@ class Parser {
if (type != null) {
if (next(TokenType.RBRACKET)) {
return new ListTypeContext(LBRACKET, type, current);
} else
throw new SyntaxError('Expected "]" in list type.', type.span);
} else
throw new SyntaxError('Expected type after "[".', LBRACKET.span);
} else {
errors.add(new SyntaxError('Missing "]" in list type.', type.span));
return null;
}
} else {
errors.add(new SyntaxError('Missing type after "[".', LBRACKET.span));
return null;
}
} else
return null;
}
@ -308,10 +349,12 @@ class Parser {
if (val != null)
return new DirectiveContext(
ARROBA, NAME, COLON, null, null, null, val);
else
throw new SyntaxError(
'Expected value or variable in directive after colon.',
COLON.span);
else {
errors.add(new SyntaxError(
'Missing value or variable in directive after colon.',
COLON.span));
return null;
}
} else if (next(TokenType.LPAREN)) {
var LPAREN = current;
var arg = parseArgument();
@ -319,16 +362,23 @@ class Parser {
if (next(TokenType.RPAREN)) {
return new DirectiveContext(
ARROBA, NAME, null, LPAREN, current, arg, null);
} else
throw new SyntaxError('Expected \')\'', arg.valueOrVariable.span);
} else
throw new SyntaxError(
'Expected argument in directive.', LPAREN.span);
} else {
errors.add(
new SyntaxError('Missing \')\'', arg.valueOrVariable.span));
return null;
}
} else {
errors.add(
new SyntaxError('Missing argument in directive.', LPAREN.span));
return null;
}
} else
return new DirectiveContext(
ARROBA, NAME, null, null, null, null, null);
} else
throw new SyntaxError('Expected name for directive.', ARROBA.span);
} else {
errors.add(new SyntaxError('Missing name for directive.', ARROBA.span));
return null;
}
} else
return null;
}
@ -341,16 +391,17 @@ class Parser {
while (arg != null) {
out.add(arg);
if (next(TokenType.COMMA))
arg = parseArgument();
else
break;
eatCommas();
arg = parseArgument();
}
if (next(TokenType.RPAREN))
return out;
else
throw new SyntaxError('Expected ")" in argument list.', LPAREN.span);
else {
errors
.add(new SyntaxError('Missing ")" in argument list.', LPAREN.span));
return null;
}
} else
return [];
}
@ -363,12 +414,16 @@ class Parser {
var val = parseValueOrVariable();
if (val != null)
return new ArgumentContext(NAME, COLON, val);
else
throw new SyntaxError(
'Expected value or variable in argument.', COLON.span);
} else
throw new SyntaxError(
'Expected colon after name in argument.', NAME.span);
else {
errors.add(new SyntaxError(
'Missing value or variable in argument.', COLON.span));
return null;
}
} else {
errors.add(new SyntaxError(
'Missing colon after name in argument.', NAME.span));
return null;
}
} else
return null;
}
@ -391,10 +446,12 @@ class Parser {
var DOLLAR = current;
if (next(TokenType.NAME))
return new VariableContext(DOLLAR, current);
else
throw new SyntaxError(
'Expected name for variable; found a lone "\$" instead.',
DOLLAR.span);
else {
errors.add(new SyntaxError(
'Missing name for variable; found a lone "\$" instead.',
DOLLAR.span));
return null;
}
} else
return null;
}
@ -405,8 +462,11 @@ class Parser {
var value = parseValue();
if (value != null) {
return new DefaultValueContext(EQUALS, value);
} else
throw new SyntaxError('Expected value after "=" sign.', EQUALS.span);
} else {
errors
.add(new SyntaxError('Missing value after "=" sign.', EQUALS.span));
return null;
}
} else
return null;
}
@ -427,14 +487,20 @@ class Parser {
}
ValueContext parseValue() {
return parseStringValue() ??
parseNumberValue() ??
return (parseNumberValue() ??
parseStringValue() ??
parseBooleanValue() ??
parseArrayValue();
parseNullValue() ??
parseEnumValue() ??
parseListValue() ??
parseObjectValue()) as ValueContext;
}
StringValueContext parseStringValue() =>
next(TokenType.STRING) ? new StringValueContext(current) : null;
StringValueContext parseStringValue() => next(TokenType.STRING)
? new StringValueContext(current)
: (next(TokenType.BLOCK_STRING)
? new StringValueContext(current, isBlockString: true)
: null);
NumberValueContext parseNumberValue() =>
next(TokenType.NUMBER) ? new NumberValueContext(current) : null;
@ -442,25 +508,84 @@ class Parser {
BooleanValueContext parseBooleanValue() =>
next(TokenType.BOOLEAN) ? new BooleanValueContext(current) : null;
ArrayValueContext parseArrayValue() {
EnumValueContext parseEnumValue() =>
next(TokenType.NAME) ? new EnumValueContext(current) : null;
NullValueContext parseNullValue() =>
next(TokenType.NULL) ? new NullValueContext(current) : null;
ListValueContext parseListValue() {
if (next(TokenType.LBRACKET)) {
var LBRACKET = current;
var lastSpan = LBRACKET.span;
List<ValueContext> values = [];
ValueContext value = parseValue();
while (value != null) {
lastSpan = value.span;
values.add(value);
if (next(TokenType.COMMA)) {
value = parseValue();
} else
break;
eatCommas();
value = parseValue();
}
eatCommas();
if (next(TokenType.RBRACKET)) {
return new ArrayValueContext(LBRACKET, current)..values.addAll(values);
} else
throw new SyntaxError('Unterminated array literal.', LBRACKET.span);
return new ListValueContext(LBRACKET, current)..values.addAll(values);
} else {
errors.add(new SyntaxError('Unterminated list literal.', lastSpan));
return null;
}
} else
return null;
}
ObjectValueContext parseObjectValue() {
if (next(TokenType.LBRACE)) {
var LBRACE = current;
var lastSpan = LBRACE.span;
var fields = <ObjectFieldContext>[];
var field = parseObjectField();
while (field != null) {
lastSpan = field.span;
eatCommas();
field = parseObjectField();
}
eatCommas();
if (next(TokenType.RBRACE)) {
return new ObjectValueContext(LBRACE, fields, current);
} else {
errors.add(new SyntaxError('Unterminated object literal.', lastSpan));
return null;
}
} else {
return null;
}
}
ObjectFieldContext parseObjectField() {
if (next(TokenType.NAME)) {
var NAME = current;
if (next(TokenType.COLON)) {
var COLON = current;
var value = parseValue();
if (value != null) {
return new ObjectFieldContext(NAME, COLON, value);
} else {
errors.add(new SyntaxError('Missing value after ":".', COLON.span));
return null;
}
} else {
errors.add(new SyntaxError(
'Missing ":" after name "${NAME.span.text}".', NAME.span));
return null;
}
} else {
return null;
}
}
}

View file

@ -7,5 +7,6 @@ class SyntaxError implements Exception {
SyntaxError(this.message, this.span);
@override
String toString() => 'Syntax error at ${span.start.toolString}: $message\n${span.highlight()}';
String toString() =>
'Syntax error at ${span.start.toolString}: $message\n${span.highlight()}';
}

View file

@ -21,6 +21,8 @@ enum TokenType {
BOOLEAN,
NUMBER,
STRING,
BLOCK_STRING,
NAME
NAME,
NULL
}

View file

@ -95,7 +95,7 @@ class GraphQLVisitor {
visitNumberValue(ctx);
else if (ctx is BooleanValueContext)
visitBooleanValue(ctx);
else if (ctx is ArrayValueContext) visitArrayValue(ctx);
else if (ctx is ListValueContext) visitArrayValue(ctx);
}
visitStringValue(StringValueContext ctx) {}
@ -104,7 +104,7 @@ class GraphQLVisitor {
visitNumberValue(NumberValueContext ctx) {}
visitArrayValue(ArrayValueContext ctx) {
visitArrayValue(ListValueContext ctx) {
ctx.values.forEach(visitValue);
}

View file

@ -17,7 +17,8 @@ main() {
}
ArgumentContext parseArgument(String text) => parse(text).parseArgument();
List<ArgumentContext> parseArgumentList(String text) => parse(text).parseArguments();
List<ArgumentContext> parseArgumentList(String text) =>
parse(text).parseArguments();
Matcher isArgument(String name, value) => new _IsArgument(name, value);
@ -60,8 +61,9 @@ class _IsArgumentList extends Matcher {
@override
bool matches(item, Map matchState) {
var args =
item is List<ArgumentContext> ? item : parse(item.toString()).parseArguments();
var args = item is List<ArgumentContext>
? item
: parse(item.toString()).parseArguments();
if (args.length != arguments.length) return false;

View file

@ -58,7 +58,8 @@ class _IsDirective extends Matcher {
@override
bool matches(item, Map matchState) {
var directive = item is DirectiveContext ? item : parseDirective(item.toString());
var directive =
item is DirectiveContext ? item : parseDirective(item.toString());
if (directive == null) return false;
if (valueOrVariable != null) {
if (directive.valueOrVariable == null)
@ -90,8 +91,9 @@ class _IsDirectiveList extends Matcher {
@override
bool matches(item, Map matchState) {
var args =
item is List<DirectiveContext> ? item : parse(item.toString()).parseDirectives();
var args = item is List<DirectiveContext>
? item
: parse(item.toString()).parseDirectives();
if (args.length != directives.length) return false;

View file

@ -116,15 +116,18 @@ class _IsFieldName extends Matcher {
@override
Description describe(Description description) {
if (realName != null)
return description.add('is field with name "$name" and alias "$realName"');
return description
.add('is field with name "$name" and alias "$realName"');
return description.add('is field with name "$name"');
}
@override
bool matches(item, Map matchState) {
var fieldName = item is FieldNameContext ? item : parseFieldName(item.toString());
var fieldName =
item is FieldNameContext ? item : parseFieldName(item.toString());
if (realName != null)
return fieldName.alias?.alias == name && fieldName.alias?.name == realName;
return fieldName.alias?.alias == name &&
fieldName.alias?.name == realName;
else
return fieldName.name == name;
}

View file

@ -43,8 +43,9 @@ class _IsFragmentSpread extends Matcher {
@override
bool matches(item, Map matchState) {
var spread =
item is FragmentSpreadContext ? item : parseFragmentSpread(item.toString());
var spread = item is FragmentSpreadContext
? item
: parseFragmentSpread(item.toString());
if (spread == null) return false;
if (spread.name != name) return false;
if (directives != null)

View file

@ -58,8 +58,9 @@ class _IsInlineFragment extends Matcher {
@override
bool matches(item, Map matchState) {
var fragment =
item is InlineFragmentContext ? item : parseInlineFragment(item.toString());
var fragment = item is InlineFragmentContext
? item
: parseInlineFragment(item.toString());
if (fragment == null) return false;
if (fragment.typeCondition.typeName.name != name) return false;
if (directives != null &&

View file

@ -66,7 +66,8 @@ class _IsSelectionSet extends Matcher {
@override
bool matches(item, Map matchState) {
var set = item is SelectionSetContext ? item : parseSelectionSet(item.toString());
var set =
item is SelectionSetContext ? item : parseSelectionSet(item.toString());
if (set == null) return false;
if (set.selections.length != selections.length) return false;

View file

@ -10,8 +10,8 @@ It's easy to define a schema with the
```dart
final GraphQLSchema todoSchema = new GraphQLSchema(
query: objectType('Todo', [
field('text', type: graphQLString.nonNullable()),
field('created_at', type: graphQLDate)
field('text', graphQLString.nonNullable()),
field('created_at', graphQLDate)
]));
```

View file

@ -1,15 +1,15 @@
import 'package:graphql_schema/graphql_schema.dart';
final GraphQLSchema todoSchema = new GraphQLSchema(
query: objectType('Todo', fields: [
queryType: objectType('Todo', fields: [
field(
'text',
type: graphQLString.nonNullable(),
graphQLString.nonNullable(),
resolve: resolveToNull,
),
field(
'created_at',
type: graphQLDate,
graphQLDate,
resolve: resolveToNull,
),
]),
@ -17,7 +17,7 @@ final GraphQLSchema todoSchema = new GraphQLSchema(
main() {
// Validation
var validation = todoSchema.query.validate(
var validation = todoSchema.queryType.validate(
'@root',
{
'foo': 'bar',
@ -34,7 +34,7 @@ main() {
}
// Serialization
print(todoSchema.query.serialize({
print(todoSchema.queryType.serialize({
'text': 'Clean your room!',
'created_at': new DateTime.now().subtract(new Duration(days: 10))
}));

View file

@ -1 +1 @@
export 'src/schema.dart';
export 'src/schema.dart';

View file

@ -1,14 +1,42 @@
part of graphql_schema.src.schema;
class GraphQLFieldArgument<Value, Serialized> {
class GraphQLFieldInput<Value, Serialized> {
final String name;
final GraphQLType<Value, Serialized> type;
final Value defaultValue;
final String description;
/// If [defaultValue] is `null`, and `null` is a valid value for this argument, set this to `true`.
/// If [defaultValue] is `null`, and `null` is a valid value for this parameter, set this to `true`.
final bool defaultsToNull;
GraphQLFieldArgument(this.name, this.type,
{this.defaultValue, this.defaultsToNull: false, this.description});
static bool _isInputTypeOrScalar(GraphQLType type) {
if (type is GraphQLInputObjectType) {
return true;
} else if (type is GraphQLUnionType) {
return type.possibleTypes.every(_isInputTypeOrScalar);
} else if (type is GraphQLObjectType) {
return false;
} else if (type is GraphQLNonNullableType) {
return _isInputTypeOrScalar(type.ofType);
} else if (type is GraphQLListType) {
return _isInputTypeOrScalar(type.ofType);
} else {
return true;
}
}
GraphQLFieldInput(this.name, this.type,
{this.defaultValue, this.defaultsToNull: false, this.description}) {
assert(_isInputTypeOrScalar(type),
'All inputs to a GraphQL field must either be scalar types, or explicitly marked as INPUT_OBJECT. Call `GraphQLObjectType.asInputObject()` on any object types you are passing as inputs to a field.');
}
@override
bool operator ==(other) =>
other is GraphQLFieldInput &&
other.name == name &&
other.type == type &&
other.defaultValue == other.defaultValue &&
other.defaultsToNull == defaultsToNull &&
other.description == description;
}

View file

@ -36,12 +36,27 @@ class GraphQLEnumType<Value> extends GraphQLScalarType<Value, String>
@override
ValidationResult<String> validate(String key, String input) {
if (!values.any((v) => v.name == input)) {
if (input == null) {
return new ValidationResult<String>._failure(
['The enum "$name" does not accept null values.']);
}
return new ValidationResult<String>._failure(
['"$input" is not a valid value for the enum "$name".']);
}
return new ValidationResult<String>._ok(input);
}
@override
bool operator ==(other) =>
other is GraphQLEnumType &&
other.name == name &&
other.description == description &&
const ListEquality<GraphQLEnumValue>().equals(other.values, values);
@override
GraphQLType<Value, String> coerceToInputObject() => this;
}
class GraphQLEnumValue<Value> {
@ -50,10 +65,16 @@ class GraphQLEnumValue<Value> {
final String description;
final String deprecationReason;
GraphQLEnumValue(this.name, this.value, {this.description, this.deprecationReason});
GraphQLEnumValue(this.name, this.value,
{this.description, this.deprecationReason});
bool get isDeprecated => deprecationReason != null;
@override
bool operator ==(other) => other is GraphQLEnumValue && other.name == name;
bool operator ==(other) =>
other is GraphQLEnumValue &&
other.name == name &&
other.value == value &&
other.description == description &&
other.deprecationReason == deprecationReason;
}

View file

@ -3,19 +3,23 @@ part of graphql_schema.src.schema;
typedef FutureOr<Value> GraphQLFieldResolver<Value, Serialized>(
Serialized serialized, Map<String, dynamic> argumentValues);
class GraphQLField<Value, Serialized> {
final List<GraphQLFieldArgument> arguments = <GraphQLFieldArgument>[];
class GraphQLObjectField<Value, Serialized> {
final List<GraphQLFieldInput> inputs = <GraphQLFieldInput>[];
final String name;
final GraphQLFieldResolver<Value, Serialized> resolve;
final GraphQLType<Value, Serialized> type;
final String description;
final String deprecationReason;
GraphQLField(this.name,
{Iterable<GraphQLFieldArgument> arguments: const <GraphQLFieldArgument>[],
GraphQLObjectField(this.name, this.type,
{Iterable<GraphQLFieldInput> arguments: const <GraphQLFieldInput>[],
@required this.resolve,
this.type,
this.deprecationReason}) {
this.arguments.addAll(arguments ?? <GraphQLFieldArgument>[]);
this.deprecationReason,
this.description}) {
assert(type != null, 'GraphQL fields must specify a `type`.');
assert(
resolve != null, 'GraphQL fields must specify a `resolve` callback.');
this.inputs.addAll(arguments ?? <GraphQLFieldInput>[]);
}
bool get isDeprecated => deprecationReason?.isNotEmpty == true;
@ -29,4 +33,13 @@ class GraphQLField<Value, Serialized> {
if (resolve != null) return resolve(serialized, argumentValues);
return type.deserialize(serialized);
}
@override
bool operator ==(other) =>
other is GraphQLObjectField &&
other.name == name &&
other.deprecationReason == deprecationReason &&
other.type == type &&
other.resolve == resolve &&
const ListEquality<GraphQLFieldInput>().equals(other.inputs, inputs);
}

View file

@ -3,7 +3,7 @@ part of graphql_schema.src.schema;
GraphQLObjectType objectType(String name,
{String description,
bool isInterface: false,
Iterable<GraphQLField> fields = const [],
Iterable<GraphQLObjectField> fields = const [],
Iterable<GraphQLObjectType> interfaces = const []}) {
var obj = new GraphQLObjectType(name, description, isInterface: isInterface)
..fields.addAll(fields ?? []);
@ -17,14 +17,28 @@ GraphQLObjectType objectType(String name,
return obj;
}
GraphQLField<T, Serialized> field<T, Serialized>(String name,
{Iterable<GraphQLFieldArgument<T, Serialized>> arguments: const [],
GraphQLObjectField<T, Serialized> field<T, Serialized>(
String name, GraphQLType<T, Serialized> type,
{Iterable<GraphQLFieldInput<T, Serialized>> inputs: const [],
GraphQLFieldResolver<T, Serialized> resolve,
GraphQLType<T, Serialized> type,
String deprecationReason}) {
return new GraphQLField(name,
arguments: arguments,
resolve: resolve,
type: type,
String deprecationReason, String description}) {
return new GraphQLObjectField<T, Serialized>(name, type,
arguments: inputs,
resolve: resolve ?? (_, __) => null,
description: description,
deprecationReason: deprecationReason);
}
GraphQLInputObjectType inputObjectType(String name,
{String description,
Iterable<GraphQLInputObjectField> inputFields: const []}) {
return new GraphQLInputObjectType(name,
description: description, inputFields: inputFields);
}
GraphQLInputObjectField<T, Serialized> inputField<T, Serialized>(
String name, GraphQLType<T, Serialized> type,
{String description, T defaultValue}) {
return new GraphQLInputObjectField(name, type,
description: description, defaultValue: defaultValue);
}

View file

@ -5,7 +5,7 @@ class GraphQLObjectType
with _NonNullableMixin<Map<String, dynamic>, Map<String, dynamic>> {
final String name;
final String description;
final List<GraphQLField> fields = [];
final List<GraphQLObjectField> fields = [];
final bool isInterface;
final List<GraphQLObjectType> _interfaces = [];
@ -22,6 +22,19 @@ class GraphQLObjectType
GraphQLObjectType(this.name, this.description, {this.isInterface: false});
@override
GraphQLType<Map<String, dynamic>, Map<String, dynamic>> coerceToInputObject() {
return asInputObject('${name}Input', description: description);
}
/// Converts [this] into a [GraphQLInputObjectType].
GraphQLInputObjectType asInputObject(String name, {String description}) {
return new GraphQLInputObjectType(name,
description: description ?? this.description,
inputFields:
fields.map((f) => new GraphQLInputObjectField(f.name, f.type.coerceToInputObject())));
}
void inheritFrom(GraphQLObjectType other) {
if (!_interfaces.contains(other)) {
_interfaces.add(other);
@ -119,6 +132,19 @@ class GraphQLObjectType
return false;
}
}
@override
bool operator ==(other) {
return other is GraphQLObjectType &&
other.name == name &&
other.description == description &&
other.isInterface == isInterface &&
const ListEquality<GraphQLObjectField>().equals(other.fields, fields) &&
// const ListEquality<GraphQLObjectType>() Removed, as it causes a stack overflow :(
// .equals(other.interfaces, interfaces) &&
const ListEquality<GraphQLObjectType>()
.equals(other.possibleTypes, possibleTypes);
}
}
Map<String, dynamic> _foldToStringDynamic(Map map) {
@ -127,3 +153,112 @@ Map<String, dynamic> _foldToStringDynamic(Map map) {
: map.keys.fold<Map<String, dynamic>>(
<String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);
}
class GraphQLInputObjectType
extends GraphQLType<Map<String, dynamic>, Map<String, dynamic>>
with _NonNullableMixin<Map<String, dynamic>, Map<String, dynamic>> {
final String name;
final String description;
final List<GraphQLInputObjectField> inputFields = [];
GraphQLInputObjectType(this.name,
{this.description,
Iterable<GraphQLInputObjectField> inputFields: const []}) {
this.inputFields.addAll(inputFields ?? const <GraphQLInputObjectField>[]);
}
@override
ValidationResult<Map<String, dynamic>> validate(String key, Map input) {
if (input is! Map)
return new ValidationResult._failure(['Expected "$key" to be a Map.']);
var out = {};
List<String> errors = [];
for (var field in inputFields) {
if (field.type is GraphQLNonNullableType) {
if (!input.containsKey(field.name) || input[field.name] == null) {
errors.add(
'Field "${field.name}, of type ${field.type} cannot be null."');
}
}
}
input.keys.forEach((k) {
var field =
inputFields.firstWhere((f) => f.name == k, orElse: () => null);
if (field == null) {
errors.add(
'Unexpected field "$k" encountered in $key. Accepted values on type $name: ${inputFields.map((f) => f.name).toList()}');
} else {
var v = input[k];
var result = field.type.validate(k.toString(), v);
if (!result.successful) {
errors.addAll(result.errors.map((s) => '$key: $s'));
} else {
out[k] = v;
}
}
});
if (errors.isNotEmpty) {
return new ValidationResult._failure(errors);
} else
return new ValidationResult._ok(_foldToStringDynamic(out));
}
@override
Map<String, dynamic> serialize(Map value) {
return value.keys.fold<Map<String, dynamic>>({}, (out, k) {
var field =
inputFields.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.toString()] = field.type.serialize(value[k]);
});
}
@override
Map<String, dynamic> deserialize(Map value) {
return value.keys.fold<Map<String, dynamic>>({}, (out, k) {
var field =
inputFields.firstWhere((f) => f.name == k, orElse: () => null);
if (field == null)
throw new UnsupportedError('Unexpected field "$k" encountered in map.');
return out..[k.toString()] = field.type.deserialize(value[k]);
});
}
@override
bool operator ==(other) {
return other is GraphQLInputObjectType &&
other.name == name &&
other.description == description &&
const ListEquality<GraphQLInputObjectField>()
.equals(other.inputFields, inputFields);
}
@override
GraphQLType<Map<String, dynamic>, Map<String, dynamic>> coerceToInputObject() => this;
}
class GraphQLInputObjectField<Value, Serialized> {
final String name;
final GraphQLType<Value, Serialized> type;
final String description;
final Value defaultValue;
GraphQLInputObjectField(this.name, this.type,
{this.description, this.defaultValue});
@override
bool operator ==(other) =>
other is GraphQLInputObjectField &&
other.name == name &&
other.type == type &&
other.description == description &&
other.defaultValue == defaultValue;
}

View file

@ -10,7 +10,8 @@ final GraphQLScalarType<String, String> graphQLString =
/// The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache.
///
/// The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be humanreadable.
final GraphQLScalarType<String, String> graphQLId = new _GraphQLStringType._('ID');
final GraphQLScalarType<String, String> graphQLId =
new _GraphQLStringType._('ID');
/// A [DateTime].
final GraphQLScalarType<DateTime, String> graphQLDate =
@ -60,6 +61,9 @@ class _GraphQLBoolType extends GraphQLScalarType<bool, bool> {
bool deserialize(bool serialized) {
return serialized;
}
@override
GraphQLType<bool, bool> coerceToInputObject() => this;
}
class _GraphQLNumType<T extends num> extends GraphQLScalarType<T, T> {
@ -88,6 +92,9 @@ class _GraphQLNumType<T extends num> extends GraphQLScalarType<T, T> {
T serialize(T value) {
return value;
}
@override
GraphQLType<T, T> coerceToInputObject() => this;
}
class _GraphQLStringType extends GraphQLScalarType<String, String> {
@ -109,6 +116,9 @@ class _GraphQLStringType extends GraphQLScalarType<String, String> {
input == null || input is String
? new ValidationResult<String>._ok(input)
: new ValidationResult._failure(['Expected "$key" to be a string.']);
@override
GraphQLType<String, String> coerceToInputObject() => this;
}
class _GraphQLDateType extends GraphQLScalarType<DateTime, String>
@ -142,4 +152,7 @@ class _GraphQLDateType extends GraphQLScalarType<DateTime, String>
['$key must be an ISO 8601-formatted date string.']);
}
}
@override
GraphQLType<DateTime, String> coerceToInputObject() => this;
}

View file

@ -2,6 +2,7 @@ library graphql_schema.src.schema;
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
@ -24,19 +25,21 @@ part 'union.dart';
part 'validation_result.dart';
class GraphQLSchema {
final GraphQLObjectType query;
final GraphQLObjectType mutation;
final GraphQLObjectType subscription;
final GraphQLObjectType queryType;
final GraphQLObjectType mutationType;
final GraphQLObjectType subscriptionType;
GraphQLSchema({this.query, this.mutation, this.subscription});
GraphQLSchema({this.queryType, this.mutationType, this.subscriptionType});
}
GraphQLSchema graphQLSchema(
{@required GraphQLObjectType query,
GraphQLObjectType mutation,
GraphQLObjectType subscription}) =>
{@required GraphQLObjectType queryType,
GraphQLObjectType mutationType,
GraphQLObjectType subscriptionType}) =>
new GraphQLSchema(
query: query, mutation: mutation, subscription: subscription);
queryType: queryType,
mutationType: mutationType,
subscriptionType: subscriptionType);
/// A default resolver that always returns `null`.
resolveToNull(_, __) => null;

View file

@ -13,6 +13,8 @@ abstract class GraphQLType<Value, Serialized> {
GraphQLType<Value, Serialized> nonNullable();
GraphQLType<Value, Serialized> coerceToInputObject();
@override
String toString() => name;
}
@ -25,16 +27,16 @@ GraphQLListType<Value, Serialized> listType<Value, Serialized>(
class GraphQLListType<Value, Serialized>
extends GraphQLType<List<Value>, List<Serialized>>
with _NonNullableMixin<List<Value>, List<Serialized>> {
final GraphQLType<Value, Serialized> innerType;
final GraphQLType<Value, Serialized> ofType;
GraphQLListType(this.innerType);
GraphQLListType(this.ofType);
@override
String get name => null;
@override
String get description =>
'A list of items of type ${innerType.name ?? '(${innerType.description}).'}';
'A list of items of type ${ofType.name ?? '(${ofType.description}).'}';
@override
ValidationResult<List<Serialized>> validate(
@ -48,7 +50,7 @@ class GraphQLListType<Value, Serialized>
for (int i = 0; i < input.length; i++) {
var k = '"$key" at index $i';
var v = input[i];
var result = innerType.validate(k, v);
var result = ofType.validate(k, v);
if (!result.successful)
errors.addAll(result.errors);
else
@ -61,16 +63,23 @@ class GraphQLListType<Value, Serialized>
@override
List<Value> deserialize(List<Serialized> serialized) {
return serialized.map<Value>(innerType.deserialize).toList();
return serialized.map<Value>(ofType.deserialize).toList();
}
@override
List<Serialized> serialize(List<Value> value) {
return value.map<Serialized>(innerType.serialize).toList();
return value.map<Serialized>(ofType.serialize).toList();
}
@override
String toString() => '[$innerType]';
String toString() => '[$ofType]';
@override
bool operator ==(other) => other is GraphQLListType && other.ofType == ofType;
@override
GraphQLType<List<Value>, List<Serialized>> coerceToInputObject() =>
new GraphQLListType<Value, Serialized>(ofType.coerceToInputObject());
}
abstract class _NonNullableMixin<Value, Serialized>
@ -83,16 +92,16 @@ abstract class _NonNullableMixin<Value, Serialized>
class GraphQLNonNullableType<Value, Serialized>
extends GraphQLType<Value, Serialized> {
final GraphQLType<Value, Serialized> innerType;
final GraphQLType<Value, Serialized> ofType;
GraphQLNonNullableType._(this.innerType);
GraphQLNonNullableType._(this.ofType);
@override
String get name => innerType.name;
String get name => null; //innerType.name;
@override
String get description =>
'A non-nullable binding to ${innerType.name ?? '(${innerType.description}).'}';
'A non-nullable binding to ${ofType.name ?? '(${ofType.description}).'}';
@override
GraphQLType<Value, Serialized> nonNullable() {
@ -105,21 +114,30 @@ class GraphQLNonNullableType<Value, Serialized>
if (input == null)
return new ValidationResult._failure(
['Expected "$key" to be a non-null value.']);
return innerType.validate(key, input);
return ofType.validate(key, input);
}
@override
Value deserialize(Serialized serialized) {
return innerType.deserialize(serialized);
return ofType.deserialize(serialized);
}
@override
Serialized serialize(Value value) {
return innerType.serialize(value);
return ofType.serialize(value);
}
@override
String toString() {
return '$innerType!';
return '$ofType!';
}
@override
bool operator ==(other) =>
other is GraphQLNonNullableType && other.ofType == ofType;
@override
GraphQLType<Value, Serialized> coerceToInputObject() {
return ofType.coerceToInputObject().nonNullable();
}
}

View file

@ -7,17 +7,29 @@ class GraphQLUnionType
final List<GraphQLObjectType> possibleTypes = [];
GraphQLUnionType(
this.name,
Iterable<GraphQLObjectType> possibleTypes,
) {
this.name,
Iterable<GraphQLType<Map<String, dynamic>, Map<String, dynamic>>>
possibleTypes) {
assert(possibleTypes.every((t) => t is GraphQLObjectType),
'The member types of a Union type must all be Object base types; Scalar, Interface and Union types must not be member types of a Union. Similarly, wrapping types must not be member types of a Union.');
assert(possibleTypes.isNotEmpty,
'A Union type must define one or more member types.');
this.possibleTypes.addAll(possibleTypes.toSet());
for (var t in possibleTypes.toSet()) {
this.possibleTypes.add(t as GraphQLObjectType);
}
}
@override
String get description => possibleTypes.map((t) => t.name).join(' | ');
@override
GraphQLType<Map<String, dynamic>, Map<String, dynamic>>
coerceToInputObject() {
return new GraphQLUnionType(
'${name}Input', possibleTypes.map((t) => t.coerceToInputObject()));
}
@override
Map<String, dynamic> serialize(Map<String, dynamic> value) {
for (var type in possibleTypes) {
@ -57,4 +69,12 @@ class GraphQLUnionType
return new ValidationResult<Map<String, dynamic>>._failure(errors);
}
@override
bool operator ==(other) =>
other is GraphQLUnionType &&
other.name == name &&
other.description == description &&
const ListEquality<GraphQLObjectType>()
.equals(other.possibleTypes, possibleTypes);
}

View file

@ -15,7 +15,7 @@ class ValidationResult<T> {
: value = null,
successful = false;
ValidationResult<T> _asFailure() {
return new ValidationResult<T>._(false, value, errors);
}
}
// ValidationResult<T> _asFailure() {
// return new ValidationResult<T>._(false, value, errors);
// }
}

View file

@ -1,14 +1,15 @@
import 'package:graphql_schema/graphql_schema.dart';
final GraphQLObjectType pokemonType = objectType('Pokemon', fields:[
field('species', type: graphQLString),
field('catch_date', type: graphQLDate)
final GraphQLObjectType pokemonType = objectType('Pokemon', fields: [
field('species', graphQLString),
field('catch_date', graphQLDate)
]);
final GraphQLObjectType trainerType =
objectType('Trainer', fields:[field('name', type: graphQLString)]);
objectType('Trainer', fields: [field('name', graphQLString)]);
final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion', fields:[
field('trainer', type: trainerType),
field('pokemon_species', type: listType(pokemonType))
]);
final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion',
fields: [
field('trainer', trainerType),
field('pokemon_species', listType(pokemonType))
]);

View file

@ -51,9 +51,11 @@ main() {
]
});
expect(() => pokemonRegionType.serialize({
'trainer': trainer,
'DIGIMON_species': [pikachu, charizard]
}), throwsUnsupportedError);
expect(
() => pokemonRegionType.serialize({
'trainer': trainer,
'DIGIMON_species': [pikachu, charizard]
}),
throwsUnsupportedError);
});
}

View file

@ -11,11 +11,11 @@ void main() {
var pokemonType = objectType('Pokémon', fields: [
field(
'name',
type: graphQLString.nonNullable(),
graphQLString.nonNullable(),
),
field(
'type',
type: typeType,
typeType,
),
]);

View file

@ -29,15 +29,16 @@ class GraphQL {
allTypes.addAll(this.customTypes);
_schema = reflectSchema(_schema, allTypes);
for (var type in allTypes) {
for (var type in allTypes.toSet()) {
if (!this.customTypes.contains(type)) {
this.customTypes.add(type);
}
}
}
if (_schema.query != null) this.customTypes.add(_schema.query);
if (_schema.mutation != null) this.customTypes.add(_schema.mutation);
if (_schema.queryType != null) this.customTypes.add(_schema.queryType);
if (_schema.mutationType != null)
this.customTypes.add(_schema.mutationType);
}
GraphQLType convertType(TypeContext ctx) {
@ -76,6 +77,15 @@ class GraphQL {
var tokens = scan(text, sourceUrl: sourceUrl);
var parser = new Parser(tokens);
var document = parser.parseDocument();
if (parser.errors.isNotEmpty) {
throw new GraphQLException(parser.errors
.map((e) => new GraphQLExceptionError(e.message, locations: [
new GraphExceptionErrorLocation.fromSourceLocation(e.span.start)
]))
.toList());
}
return executeRequest(_schema, document,
operationName: operationName,
initialValue: initialValue,
@ -166,7 +176,7 @@ class GraphQL {
GraphQLSchema schema,
Map<String, dynamic> variableValues,
initialValue) async {
var queryType = schema.query;
var queryType = schema.queryType;
var selectionSet = query.selectionSet;
return await executeSelectionSet(
document, selectionSet, queryType, initialValue, variableValues);
@ -178,7 +188,7 @@ class GraphQL {
GraphQLSchema schema,
Map<String, dynamic> variableValues,
initialValue) async {
var mutationType = schema.mutation;
var mutationType = schema.mutationType;
if (mutationType == null) {
throw new GraphQLException.fromMessage(
@ -241,7 +251,7 @@ class GraphQL {
var argumentValues = field.field.arguments;
var fieldName = field.field.fieldName.name;
var desiredField = objectType.fields.firstWhere((f) => f.name == fieldName);
var argumentDefinitions = desiredField.arguments;
var argumentDefinitions = desiredField.inputs;
for (var argumentDefinition in argumentDefinitions) {
var argumentName = argumentDefinition.name;
@ -260,7 +270,7 @@ class GraphQL {
coercedValues[argumentName] = defaultValue;
} else if (argumentType is GraphQLNonNullableType) {
throw new GraphQLException.fromSourceSpan(
'Missing value for argument "$argumentName".',
'Missing value for argument "$argumentName" of field "$fieldName".',
value.valueOrVariable.span);
} else {
continue;
@ -270,21 +280,60 @@ class GraphQL {
coercedValues[argumentName] = defaultValue;
} else if (argumentType is GraphQLNonNullableType) {
throw new GraphQLException.fromMessage(
'Missing value for argument "$argumentName".');
'Missing value for argument "$argumentName" of field "$fieldName".');
} else {
continue;
}
} else {
var validation =
argumentType.validate(fieldName, value.valueOrVariable.value.value);
try {
var validation = argumentType.validate(
fieldName, value.valueOrVariable.value.value);
if (!validation.successful) {
throw new GraphQLException.fromSourceSpan(
'Coercion error for value of argument "$argumentName".',
value.valueOrVariable.span);
} else {
var coercedValue = validation.value;
coercedValues[argumentName] = coercedValue;
if (!validation.successful) {
var errors = <GraphQLExceptionError>[
new GraphQLExceptionError(
'Type coercion error for value of argument "$argumentName" of field "$fieldName".',
locations: [
new GraphExceptionErrorLocation.fromSourceLocation(
value.valueOrVariable.span.start)
],
)
];
for (var error in validation.errors) {
errors.add(
new GraphQLExceptionError(
error,
locations: [
new GraphExceptionErrorLocation.fromSourceLocation(
value.valueOrVariable.span.start)
],
),
);
}
throw new GraphQLException(errors);
} else {
var coercedValue = validation.value;
coercedValues[argumentName] = coercedValue;
}
} on TypeError catch (e) {
throw new GraphQLException(<GraphQLExceptionError>[
new GraphQLExceptionError(
'Type coercion error for value of argument "$argumentName" of field "$fieldName".',
locations: [
new GraphExceptionErrorLocation.fromSourceLocation(
value.valueOrVariable.span.start)
],
),
new GraphQLExceptionError(
e.message.toString(),
locations: [
new GraphExceptionErrorLocation.fromSourceLocation(
value.valueOrVariable.span.start)
],
),
]);
}
}
}
@ -311,7 +360,7 @@ class GraphQL {
result,
Map<String, dynamic> variableValues) async {
if (fieldType is GraphQLNonNullableType) {
var innerType = fieldType.innerType;
var innerType = fieldType.ofType;
var completedResult = completeValue(
document, fieldName, innerType, fields, result, variableValues);
@ -333,7 +382,7 @@ class GraphQL {
'Value of field "$fieldName" must be a list or iterable, got $result instead.');
}
var innerType = fieldType.innerType;
var innerType = fieldType.ofType;
var out = [];
for (var resultItem in (result as Iterable)) {
@ -376,7 +425,8 @@ class GraphQL {
throw new UnsupportedError('Unsupported type: $fieldType');
}
GraphQLObjectType resolveAbstractType(String fieldName, GraphQLType type, result) {
GraphQLObjectType resolveAbstractType(
String fieldName, GraphQLType type, result) {
List<GraphQLObjectType> possibleTypes;
if (type is GraphQLObjectType) {

View file

@ -1,7 +1,6 @@
import 'package:graphql_parser/graphql_parser.dart';
import 'package:graphql_schema/graphql_schema.dart';
// TODO: How to handle custom types???
GraphQLSchema reflectSchema(GraphQLSchema schema, List<GraphQLType> allTypes) {
for (var type in allTypes.toList()) {
var custom = _fetchAllTypesFromType(type);
@ -14,34 +13,36 @@ GraphQLSchema reflectSchema(GraphQLSchema schema, List<GraphQLType> allTypes) {
}
var objectTypes = fetchAllTypes(schema, allTypes);
var typeType = _reflectSchemaTypes();
var directiveType = _reflectDirectiveType();
allTypes.addAll(objectTypes);
Set<GraphQLType> allTypeSet;
var schemaType = objectType('__Schema', fields: [
field(
'types',
type: listType(typeType),
resolve: (_, __) => allTypes,
listType(typeType),
resolve: (_, __) => allTypeSet ??= allTypes.toSet(),
),
field(
'queryType',
type: typeType,
resolve: (_, __) => schema.query,
typeType,
resolve: (_, __) => schema.queryType,
),
field(
'mutationType',
type: typeType,
resolve: (_, __) => schema.mutation,
typeType,
resolve: (_, __) => schema.mutationType,
),
field(
'subscriptionType',
type: typeType,
resolve: (_, __) => schema.subscription,
typeType,
resolve: (_, __) => schema.subscriptionType,
),
field(
'directives',
type: listType(directiveType).nonNullable(),
listType(directiveType).nonNullable(),
resolve: (_, __) => [], // TODO: Actually fetch directives
),
]);
@ -64,18 +65,16 @@ GraphQLSchema reflectSchema(GraphQLSchema schema, List<GraphQLType> allTypes) {
_reflectEnumValueType(),
]);
var fields = <GraphQLField>[
var fields = <GraphQLObjectField>[
field(
'__schema',
type: schemaType,
schemaType,
resolve: (_, __) => schemaType,
),
field(
'__type',
type: typeType,
arguments: [
new GraphQLFieldArgument('name', graphQLString.nonNullable())
],
typeType,
inputs: [new GraphQLFieldInput('name', graphQLString.nonNullable())],
resolve: (_, args) {
var name = args['name'] as String;
return allTypes.firstWhere((t) => t.name == name,
@ -85,11 +84,11 @@ GraphQLSchema reflectSchema(GraphQLSchema schema, List<GraphQLType> allTypes) {
),
];
fields.addAll(schema.query.fields);
fields.addAll(schema.queryType.fields);
return new GraphQLSchema(
query: objectType(schema.query.name, fields: fields),
mutation: schema.mutation,
queryType: objectType(schema.queryType.name, fields: fields),
mutationType: schema.mutationType,
);
}
@ -101,11 +100,11 @@ GraphQLObjectType _reflectSchemaTypes() {
_typeType.fields.add(
field(
'ofType',
type: _reflectSchemaTypes(),
_reflectSchemaTypes(),
resolve: (type, _) {
if (type is GraphQLListType)
return type.innerType;
else if (type is GraphQLNonNullableType) return type.innerType;
return type.ofType;
else if (type is GraphQLNonNullableType) return type.ofType;
return null;
},
),
@ -114,7 +113,7 @@ GraphQLObjectType _reflectSchemaTypes() {
_typeType.fields.add(
field(
'interfaces',
type: listType(_reflectSchemaTypes().nonNullable()),
listType(_reflectSchemaTypes().nonNullable()),
resolve: (type, _) {
if (type is GraphQLObjectType) {
return type.interfaces;
@ -128,7 +127,7 @@ GraphQLObjectType _reflectSchemaTypes() {
_typeType.fields.add(
field(
'possibleTypes',
type: listType(_reflectSchemaTypes().nonNullable()),
listType(_reflectSchemaTypes().nonNullable()),
resolve: (type, _) {
if (type is GraphQLObjectType && type.isInterface) {
return type.possibleTypes;
@ -150,8 +149,8 @@ GraphQLObjectType _reflectSchemaTypes() {
fieldType.fields.add(
field(
'type',
type: _reflectSchemaTypes(),
resolve: (f, _) => (f as GraphQLField).type,
_reflectSchemaTypes(),
resolve: (f, _) => (f as GraphQLObjectField).type,
),
);
}
@ -163,8 +162,9 @@ GraphQLObjectType _reflectSchemaTypes() {
inputValueType.fields.add(
field(
'type',
type: _reflectSchemaTypes(),
resolve: (f, _) => (f as GraphQLFieldArgument).type,
_reflectSchemaTypes(),
resolve: (f, _) =>
_fetchFromInputValue(f, (f) => f.type, (f) => f.type),
),
);
}
@ -193,17 +193,17 @@ GraphQLObjectType _createTypeType() {
return objectType('__Type', fields: [
field(
'name',
type: graphQLString,
graphQLString,
resolve: (type, _) => (type as GraphQLType).name,
),
field(
'description',
type: graphQLString,
graphQLString,
resolve: (type, _) => (type as GraphQLType).description,
),
field(
'kind',
type: _typeKindType,
_typeKindType,
resolve: (type, _) {
var t = type as GraphQLType;
@ -211,6 +211,8 @@ GraphQLObjectType _createTypeType() {
return 'ENUM';
else if (t is GraphQLScalarType)
return 'SCALAR';
else if (t is GraphQLInputObjectType)
return 'INPUT_OBJECT';
else if (t is GraphQLObjectType)
return t.isInterface ? 'INTERFACE' : 'OBJECT';
else if (t is GraphQLListType)
@ -225,9 +227,9 @@ GraphQLObjectType _createTypeType() {
),
field(
'fields',
type: listType(fieldType),
arguments: [
new GraphQLFieldArgument(
listType(fieldType),
inputs: [
new GraphQLFieldInput(
'includeDeprecated',
graphQLBoolean,
defaultValue: false,
@ -238,13 +240,13 @@ GraphQLObjectType _createTypeType() {
.where(
(f) => !f.isDeprecated || args['includeDeprecated'] == true)
.toList()
: [],
: null,
),
field(
'enumValues',
type: listType(enumValueType.nonNullable()),
arguments: [
new GraphQLFieldArgument(
listType(enumValueType.nonNullable()),
inputs: [
new GraphQLFieldInput(
'includeDeprecated',
graphQLBoolean,
defaultValue: false,
@ -263,10 +265,13 @@ GraphQLObjectType _createTypeType() {
),
field(
'inputFields',
type: listType(inputValueType.nonNullable()),
listType(inputValueType.nonNullable()),
resolve: (obj, _) {
// TODO: INPUT_OBJECT type
return <GraphQLFieldArgument>[];
if (obj is GraphQLInputObjectType) {
return obj.inputFields;
}
return null;
},
),
]);
@ -288,46 +293,64 @@ GraphQLObjectType _createFieldType() {
return objectType('__Field', fields: [
field(
'name',
type: graphQLString,
resolve: (f, _) => (f as GraphQLField).name,
graphQLString,
resolve: (f, _) => (f as GraphQLObjectField).name,
),
field(
'description',
graphQLString,
resolve: (f, _) => (f as GraphQLObjectField).description,
),
field(
'isDeprecated',
type: graphQLBoolean,
resolve: (f, _) => (f as GraphQLField).isDeprecated,
graphQLBoolean,
resolve: (f, _) => (f as GraphQLObjectField).isDeprecated,
),
field(
'deprecationReason',
type: graphQLString,
resolve: (f, _) => (f as GraphQLField).deprecationReason,
graphQLString,
resolve: (f, _) => (f as GraphQLObjectField).deprecationReason,
),
field(
'args',
type: listType(inputValueType.nonNullable()).nonNullable(),
resolve: (f, _) => (f as GraphQLField).arguments,
listType(inputValueType.nonNullable()).nonNullable(),
resolve: (f, _) => (f as GraphQLObjectField).inputs,
),
]);
}
GraphQLObjectType _inputValueType;
T _fetchFromInputValue<T>(x, T Function(GraphQLFieldInput) ifInput,
T Function(GraphQLInputObjectField) ifObjectField) {
if (x is GraphQLFieldInput) {
return ifInput(x);
} else if (x is GraphQLInputObjectField) {
return ifObjectField(x);
} else {
return null;
}
}
GraphQLObjectType _reflectInputValueType() {
return _inputValueType ??= objectType('__InputValue', fields: [
field(
'name',
type: graphQLString.nonNullable(),
resolve: (obj, _) => (obj as GraphQLFieldArgument).name,
graphQLString.nonNullable(),
resolve: (obj, _) =>
_fetchFromInputValue(obj, (f) => f.name, (f) => f.name),
),
field(
'description',
type: graphQLString,
resolve: (obj, _) => (obj as GraphQLFieldArgument).description,
graphQLString,
resolve: (obj, _) =>
_fetchFromInputValue(obj, (f) => f.description, (f) => f.description),
),
field(
'defaultValue',
type: graphQLString,
resolve: (obj, _) =>
(obj as GraphQLFieldArgument).defaultValue?.toString(),
graphQLString,
resolve: (obj, _) => _fetchFromInputValue(obj,
(f) => f.defaultValue?.toString(), (f) => f.defaultValue?.toString()),
),
]);
}
@ -351,23 +374,23 @@ GraphQLObjectType _reflectDirectiveType() {
return _directiveType ??= objectType('__Directive', fields: [
field(
'name',
type: graphQLString.nonNullable(),
graphQLString.nonNullable(),
resolve: (obj, _) => (obj as DirectiveContext).NAME.span.text,
),
field(
'description',
type: graphQLString,
graphQLString,
resolve: (obj, _) => null,
),
field(
'locations',
type: listType(_directiveLocationType.nonNullable()).nonNullable(),
listType(_directiveLocationType.nonNullable()).nonNullable(),
// TODO: Fetch directiveLocation
resolve: (obj, _) => <String>[],
),
field(
'args',
type: listType(inputValueType.nonNullable()).nonNullable(),
listType(inputValueType.nonNullable()).nonNullable(),
resolve: (obj, _) => [],
),
]);
@ -381,22 +404,22 @@ GraphQLObjectType _reflectEnumValueType() {
fields: [
field(
'name',
type: graphQLString.nonNullable(),
graphQLString.nonNullable(),
resolve: (obj, _) => (obj as GraphQLEnumValue).name,
),
field(
'description',
type: graphQLString,
graphQLString,
resolve: (obj, _) => (obj as GraphQLEnumValue).description,
),
field(
'isDeprecated',
type: graphQLBoolean.nonNullable(),
graphQLBoolean.nonNullable(),
resolve: (obj, _) => (obj as GraphQLEnumValue).isDeprecated,
),
field(
'deprecationReason',
type: graphQLString,
graphQLString,
resolve: (obj, _) => (obj as GraphQLEnumValue).deprecationReason,
),
],
@ -407,24 +430,13 @@ List<GraphQLType> fetchAllTypes(
GraphQLSchema schema, List<GraphQLType> allTypes) {
var types = <GraphQLType>[];
types.addAll(_fetchAllTypesFromObject(schema.query));
types.addAll(_fetchAllTypesFromObject(schema.queryType));
if (schema.mutation != null) {
types.addAll(_fetchAllTypesFromObject(schema.mutation)
.where((t) => t is GraphQLObjectType));
if (schema.mutationType != null) {
types.addAll(_fetchAllTypesFromObject(schema.mutationType));
}
return types;
// var types = <GraphQLObjectType>[];
//
// for (var type in typess) {
// if (type is GraphQLObjectType)
// types.add(type);
// else if (!allTypes.contains(type)) allTypes.add(type);
// }
//
// return types.toSet().toList();
}
List<GraphQLType> _fetchAllTypesFromObject(GraphQLObjectType objectType) {
@ -433,12 +445,16 @@ List<GraphQLType> _fetchAllTypesFromObject(GraphQLObjectType objectType) {
for (var field in objectType.fields) {
if (field.type is GraphQLObjectType) {
types.addAll(_fetchAllTypesFromObject(field.type as GraphQLObjectType));
} else if (field.type is GraphQLInputObjectType) {
for (var v in (field.type as GraphQLInputObjectType).inputFields) {
types.addAll(_fetchAllTypesFromType(v.type));
}
} else {
types.addAll(_fetchAllTypesFromType(field.type));
}
for (var argument in field.arguments ?? <GraphQLFieldArgument>[]) {
types.addAll(_fetchAllTypesFromType(argument.type));
for (var input in field.inputs ?? <GraphQLFieldInput>[]) {
types.addAll(_fetchAllTypesFromType(input.type));
}
}
@ -453,12 +469,18 @@ Iterable<GraphQLType> _fetchAllTypesFromType(GraphQLType type) {
var types = <GraphQLType>[];
if (type is GraphQLNonNullableType) {
types.addAll(_fetchAllTypesFromType(type.innerType));
types.addAll(_fetchAllTypesFromType(type.ofType));
} else if (type is GraphQLListType) {
types.addAll(_fetchAllTypesFromType(type.innerType));
types.addAll(_fetchAllTypesFromType(type.ofType));
} else if (type is GraphQLObjectType) {
types.addAll(_fetchAllTypesFromObject(type));
} else if (type is GraphQLEnumType) {
types.add(type);
} else if (type is GraphQLInputObjectType) {
for (var v in type.inputFields) {
types.addAll(_fetchAllTypesFromType(v.type));
}
types.add(type);
} else if (type is GraphQLUnionType) {
types.add(type);

View file

@ -21,8 +21,7 @@ GraphQLObjectType convertDartClass(Type type, [List<Type> typeArguments]) {
return convertDartType(type, typeArguments) as GraphQLObjectType;
}
final Map<Type, GraphQLType> _cache =
<Type, GraphQLType>{};
final Map<Type, GraphQLType> _cache = <Type, GraphQLType>{};
GraphQLType _objectTypeFromDartType(Type type, [List<Type> typeArguments]) {
if (type == bool) {
@ -73,10 +72,9 @@ GraphQLType _objectTypeFromDartType(Type type, [List<Type> typeArguments]) {
GraphQLObjectType objectTypeFromClassMirror(ClassMirror mirror) {
if (_cache[mirror.reflectedType] != null) {
return _cache[mirror.reflectedType] as GraphQLObjectType;
} else {
}
} else {}
var fields = <GraphQLField>[];
var fields = <GraphQLObjectField>[];
var ready = <Symbol, MethodMirror>{};
var forward = <Symbol, MethodMirror>{};
@ -219,7 +217,7 @@ GraphQLEnumType enumTypeFromClassMirror(ClassMirror mirror) {
);
}
GraphQLField fieldFromGetter(
GraphQLObjectField fieldFromGetter(
Symbol name, MethodMirror mirror, Exclude exclude, ClassMirror clazz) {
var type = _getProvidedType(mirror.metadata);
var wasProvided = type != null;
@ -242,7 +240,7 @@ GraphQLField fieldFromGetter(
return field(
nameString,
type: type,
type,
deprecationReason: _getDeprecationReason(mirror.metadata),
resolve: (obj, _) {
if (obj is Map && exclude?.canSerialize != true) {

View file

@ -3,10 +3,12 @@ environment:
sdk: ">=1.8.0 <3.0.0"
dependencies:
angel_serialize: ^2.0.0
collection: ^1.0.0
graphql_schema:
path: ../graphql_schema
graphql_parser:
path: ../graphql_parser
meta: ^1.0.0
recase: ^1.0.0
tuple: ^1.0.0
dev_dependencies:

View file

@ -4,24 +4,24 @@ import 'package:test/test.dart';
void main() {
test('single element', () async {
var todoType = objectType('todo',fields: [
var todoType = objectType('todo', fields: [
field(
'text',
type: graphQLString,
graphQLString,
resolve: (obj, args) => obj.text,
),
field(
'completed',
type: graphQLBoolean,
graphQLBoolean,
resolve: (obj, args) => obj.completed,
),
]);
var schema = graphQLSchema(
query: objectType('api', fields:[
queryType: objectType('api', fields: [
field(
'todos',
type: listType(todoType),
listType(todoType),
resolve: (_, __) => [
new Todo(
text: 'Clean your room!',