diff --git a/.idea/runConfigurations/tests_in_query_test_dart.xml b/.idea/runConfigurations/tests_in_query_test_dart.xml new file mode 100644 index 00000000..9ab72dfa --- /dev/null +++ b/.idea/runConfigurations/tests_in_query_test_dart.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/graphql_parser/lib/src/language/ast/operation_definition.dart b/graphql_parser/lib/src/language/ast/operation_definition.dart index 3246e173..e6abaf53 100644 --- a/graphql_parser/lib/src/language/ast/operation_definition.dart +++ b/graphql_parser/lib/src/language/ast/operation_definition.dart @@ -1,7 +1,8 @@ +import 'package:source_span/source_span.dart'; + import '../token.dart'; import 'definition.dart'; import 'directive.dart'; -import 'package:source_span/source_span.dart'; import 'selection_set.dart'; import 'variable_definitions.dart'; @@ -12,7 +13,8 @@ class OperationDefinitionContext extends DefinitionContext { final SelectionSetContext selectionSet; bool get isMutation => TYPE?.text == 'mutation'; - bool get isQuery => TYPE?.text == 'query'; + + bool get isQuery => TYPE?.text == 'query' || TYPE == null; String get name => NAME?.text; diff --git a/graphql_parser/lib/src/language/ast/string_value.dart b/graphql_parser/lib/src/language/ast/string_value.dart index db96fe05..a5c54b62 100644 --- a/graphql_parser/lib/src/language/ast/string_value.dart +++ b/graphql_parser/lib/src/language/ast/string_value.dart @@ -55,8 +55,8 @@ class StringValueContext extends ValueContext { buf.writeCharCode(next); } } else - throw new SyntaxError.fromSourceLocation( - 'Unexpected "\\" in string literal.', span.start); + throw new SyntaxError( + 'Unexpected "\\" in string literal.', span); } else { buf.writeCharCode(ch); } diff --git a/graphql_parser/lib/src/language/lexer.dart b/graphql_parser/lib/src/language/lexer.dart index 02309395..87000efd 100644 --- a/graphql_parser/lib/src/language/lexer.dart +++ b/graphql_parser/lib/src/language/lexer.dart @@ -35,9 +35,9 @@ final Map _patterns = { _name: TokenType.NAME }; -List scan(String text) { +List scan(String text, {sourceUrl}) { List out = []; - var scanner = new SpanScanner(text); + var scanner = new SpanScanner(text, sourceUrl: sourceUrl); while (!scanner.isDone) { List potential = []; @@ -54,7 +54,7 @@ List scan(String text) { if (potential.isEmpty) { var ch = new String.fromCharCode(scanner.readChar()); throw new SyntaxError( - "Unexpected token '$ch'.", scanner.state.line, scanner.state.column); + "Unexpected token '$ch'.", scanner.emptySpan); } else { // Choose longest token potential.sort((a, b) => b.text.length.compareTo(a.text.length)); diff --git a/graphql_parser/lib/src/language/parser.dart b/graphql_parser/lib/src/language/parser.dart index b7ec8a55..45f4e780 100644 --- a/graphql_parser/lib/src/language/parser.dart +++ b/graphql_parser/lib/src/language/parser.dart @@ -66,13 +66,13 @@ class Parser { TYPE, NAME, variables, selectionSet) ..directives.addAll(dirs); else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected selection set in fragment definition.', - NAME.span.end); + NAME.span); } else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected name after operation type "${TYPE.text}" in operation definition.', - TYPE.span.end); + TYPE.span); } else return null; } @@ -94,21 +94,21 @@ class Parser { FRAGMENT, NAME, ON, typeCondition, selectionSet) ..directives.addAll(dirs); else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected selection set in fragment definition.', - typeCondition.span.end); + typeCondition.span); } else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected type condition after "on" in fragment definition.', - ON.span.end); + ON.span); } else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected "on" after name "${NAME.text}" in fragment definition.', - NAME.span.end); + NAME.span); } else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected name after "fragment" in fragment definition.', - FRAGMENT.span.end); + FRAGMENT.span); } else return null; } @@ -142,18 +142,18 @@ class Parser { ELLIPSIS, ON, typeCondition, selectionSet) ..directives.addAll(directives); } else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected selection set in inline fragment.', directives.isEmpty - ? typeCondition.span.end - : directives.last.span.end); + ? typeCondition.span + : directives.last.span); } else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected type condition after "on" in inline fragment.', - ON.span.end); + ON.span); } else - throw new SyntaxError.fromSourceLocation( - 'Expected "on" after "..." in inline fragment.', ELLIPSIS.span.end); + throw new SyntaxError( + 'Expected "on" after "..." in inline fragment.', ELLIPSIS.span); } else return null; } @@ -174,9 +174,9 @@ class Parser { return new SelectionSetContext(LBRACE, current) ..selections.addAll(selections); else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected "}" after selection set.', - selections.isEmpty ? LBRACE.span.end : selections.last.span.end); + selections.isEmpty ? LBRACE.span : selections.last.span); } else return null; } @@ -216,8 +216,8 @@ class Parser { return new FieldNameContext( null, new AliasContext(NAME1, COLON, current)); else - throw new SyntaxError.fromSourceLocation( - 'Expected name after colon in alias.', COLON.span.end); + throw new SyntaxError( + 'Expected name after colon in alias.', COLON.span); } else return new FieldNameContext(NAME1); } else @@ -240,8 +240,8 @@ class Parser { return new VariableDefinitionsContext(LPAREN, current) ..variableDefinitions.addAll(defs); else - throw new SyntaxError.fromSourceLocation( - 'Expected ")" after variable definitions.', LPAREN.span.end); + throw new SyntaxError( + 'Expected ")" after variable definitions.', LPAREN.span); } else return null; } @@ -257,11 +257,11 @@ class Parser { return new VariableDefinitionContext( variable, COLON, type, defaultValue); } else - throw new SyntaxError.fromSourceLocation( - 'Expected type in variable definition.', COLON.span.end); + throw new SyntaxError( + 'Expected type in variable definition.', COLON.span); } else - throw new SyntaxError.fromSourceLocation( - 'Expected ":" in variable definition.', variable.span.end); + throw new SyntaxError( + 'Expected ":" in variable definition.', variable.span); } else return null; } @@ -287,11 +287,11 @@ class Parser { if (next(TokenType.RBRACKET)) { return new ListTypeContext(LBRACKET, type, current); } else - throw new SyntaxError.fromSourceLocation( - 'Expected "]" in list type.', type.span.end); + throw new SyntaxError( + 'Expected "]" in list type.', type.span); } else - throw new SyntaxError.fromSourceLocation( - 'Expected type after "[".', LBRACKET.span.end); + throw new SyntaxError( + 'Expected type after "[".', LBRACKET.span); } else return null; } @@ -320,9 +320,9 @@ class Parser { return new DirectiveContext( ARROBA, NAME, COLON, null, null, null, val); else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected value or variable in directive after colon.', - COLON.span.end); + COLON.span); } else if (next(TokenType.LPAREN)) { var LPAREN = current; var arg = parseArgument(); @@ -331,17 +331,17 @@ class Parser { return new DirectiveContext( ARROBA, NAME, null, LPAREN, current, arg, null); } else - throw new SyntaxError.fromSourceLocation( - 'Expected \')\'', arg.valueOrVariable.span.end); + throw new SyntaxError( + 'Expected \')\'', arg.valueOrVariable.span); } else - throw new SyntaxError.fromSourceLocation( - 'Expected argument in directive.', LPAREN.span.end); + throw new SyntaxError( + 'Expected argument in directive.', LPAREN.span); } else return new DirectiveContext( ARROBA, NAME, null, null, null, null, null); } else - throw new SyntaxError.fromSourceLocation( - 'Expected name for directive.', ARROBA.span.end); + throw new SyntaxError( + 'Expected name for directive.', ARROBA.span); } else return null; } @@ -363,8 +363,8 @@ class Parser { if (next(TokenType.RPAREN)) return out; else - throw new SyntaxError.fromSourceLocation( - 'Expected ")" in argument list.', LPAREN.span.end); + throw new SyntaxError( + 'Expected ")" in argument list.', LPAREN.span); } else return []; } @@ -378,11 +378,11 @@ class Parser { if (val != null) return new ArgumentContext(NAME, COLON, val); else - throw new SyntaxError.fromSourceLocation( - 'Expected value or variable in argument.', COLON.span.end); + throw new SyntaxError( + 'Expected value or variable in argument.', COLON.span); } else - throw new SyntaxError.fromSourceLocation( - 'Expected colon after name in argument.', NAME.span.end); + throw new SyntaxError( + 'Expected colon after name in argument.', NAME.span); } else return null; } @@ -406,9 +406,9 @@ class Parser { if (next(TokenType.NAME)) return new VariableContext(DOLLAR, current); else - throw new SyntaxError.fromSourceLocation( + throw new SyntaxError( 'Expected name for variable; found a lone "\$" instead.', - DOLLAR.span.end); + DOLLAR.span); } else return null; } @@ -420,8 +420,8 @@ class Parser { if (value != null) { return new DefaultValueContext(EQUALS, value); } else - throw new SyntaxError.fromSourceLocation( - 'Expected value after "=" sign.', EQUALS.span.end); + throw new SyntaxError( + 'Expected value after "=" sign.', EQUALS.span); } else return null; } @@ -474,8 +474,8 @@ class Parser { if (next(TokenType.RBRACKET)) { return new ArrayValueContext(LBRACKET, current)..values.addAll(values); } else - throw new SyntaxError.fromSourceLocation( - 'Unterminated array literal.', LBRACKET.span.end); + throw new SyntaxError( + 'Unterminated array literal.', LBRACKET.span); } else return null; } diff --git a/graphql_parser/lib/src/language/syntax_error.dart b/graphql_parser/lib/src/language/syntax_error.dart index 0929a27b..c06a8659 100644 --- a/graphql_parser/lib/src/language/syntax_error.dart +++ b/graphql_parser/lib/src/language/syntax_error.dart @@ -1,18 +1,11 @@ import 'package:source_span/source_span.dart'; -import 'token.dart'; class SyntaxError implements Exception { final String message; - final int line, column; - final Token offendingToken; + final FileSpan span; - SyntaxError(this.message, this.line, this.column, [this.offendingToken]); - - factory SyntaxError.fromSourceLocation( - String message, SourceLocation location, - [Token offendingToken]) => - new SyntaxError(message, location.line, location.column, offendingToken); + SyntaxError(this.message, this.span); @override - String toString() => 'Syntax error at line $line, column $column: $message'; + String toString() => 'Syntax error at ${span.start.toolString}: $message\n${span.highlight()}'; } diff --git a/graphql_schema/example/todo.dart b/graphql_schema/example/todo.dart index 5561c806..b837d612 100644 --- a/graphql_schema/example/todo.dart +++ b/graphql_schema/example/todo.dart @@ -4,9 +4,14 @@ final GraphQLSchema todoSchema = new GraphQLSchema( query: objectType('Todo', [ field( 'text', - innerType: graphQLString.nonNullable(), + type: graphQLString.nonNullable(), + resolve: resolveToNull, + ), + field( + 'created_at', + type: graphQLDate, + resolve: resolveToNull, ), - field('created_at', innerType: graphQLDate), ]), ); @@ -14,7 +19,11 @@ main() { // Validation var validation = todoSchema.query.validate( '@root', - {'foo': 'bar', 'text': null, 'created_at': 24,}, + { + 'foo': 'bar', + 'text': null, + 'created_at': 24, + }, ); if (validation.successful) { diff --git a/graphql_schema/lib/src/field.dart b/graphql_schema/lib/src/field.dart index d50c3827..f369f299 100644 --- a/graphql_schema/lib/src/field.dart +++ b/graphql_schema/lib/src/field.dart @@ -11,7 +11,7 @@ class GraphQLField { GraphQLField(this.name, {Iterable arguments: const [], - this.resolve, + @required this.resolve, this.type}) { this.arguments.addAll(arguments ?? []); } diff --git a/graphql_schema/lib/src/gen.dart b/graphql_schema/lib/src/gen.dart index e34575ca..5b833bf2 100644 --- a/graphql_schema/lib/src/gen.dart +++ b/graphql_schema/lib/src/gen.dart @@ -5,10 +5,9 @@ GraphQLObjectType objectType(String name, new GraphQLObjectType(name)..fields.addAll(fields ?? []); GraphQLField field(String name, - {Iterable> arguments: - const >[], + {Iterable> arguments: const [], GraphQLFieldResolver resolve, - GraphQLType innerType}) { + GraphQLType type}) { return new GraphQLField(name, - arguments: arguments, resolve: resolve, type: innerType); + arguments: arguments, resolve: resolve, type: type); } diff --git a/graphql_schema/lib/src/schema.dart b/graphql_schema/lib/src/schema.dart index 35c8c450..cfbb90d2 100644 --- a/graphql_schema/lib/src/schema.dart +++ b/graphql_schema/lib/src/schema.dart @@ -20,3 +20,6 @@ class GraphQLSchema { GraphQLSchema graphQLSchema( {@required GraphQLObjectType query, GraphQLObjectType mutation}) => new GraphQLSchema(query: query, mutation: mutation); + +/// A default resolver that always returns `null`. +resolveToNull(_, __) => null; diff --git a/graphql_schema/pubspec.yaml b/graphql_schema/pubspec.yaml index 7ea8d836..d13510f7 100644 --- a/graphql_schema/pubspec.yaml +++ b/graphql_schema/pubspec.yaml @@ -5,5 +5,7 @@ author: Tobe O homepage: https://github.com/thosakwe/graphql_schema environment: sdk: ">=1.8.0 <3.0.0" +dependencies: + meta: ^1.0.0 dev_dependencies: test: ^0.12.0 \ No newline at end of file diff --git a/graphql_schema/test/common.dart b/graphql_schema/test/common.dart index f0147bc2..5b5ae5d2 100644 --- a/graphql_schema/test/common.dart +++ b/graphql_schema/test/common.dart @@ -1,14 +1,14 @@ import 'package:graphql_schema/graphql_schema.dart'; final GraphQLObjectType pokemonType = objectType('Pokemon', [ - field('species', innerType: graphQLString), - field('catch_date', innerType: graphQLDate) + field('species', type: graphQLString), + field('catch_date', type: graphQLDate) ]); final GraphQLObjectType trainerType = - objectType('Trainer', [field('name', innerType: graphQLString)]); + objectType('Trainer', [field('name', type: graphQLString)]); final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion', [ - field('trainer', innerType: trainerType), - field('pokemon_species', innerType: listType(pokemonType)) + field('trainer', type: trainerType), + field('pokemon_species', type: listType(pokemonType)) ]); diff --git a/graphql_server/lib/graphql.dart b/graphql_server/lib/graphql_server.dart similarity index 89% rename from graphql_server/lib/graphql.dart rename to graphql_server/lib/graphql_server.dart index dd5d5cfe..a22fb8b5 100644 --- a/graphql_server/lib/graphql.dart +++ b/graphql_server/lib/graphql_server.dart @@ -44,9 +44,25 @@ class GraphQL { } } + Future> parseAndExecute(String text, + {String operationName, + sourceUrl, + Map variableValues: const {}, + initialValue}) { + var tokens = scan(text, sourceUrl: sourceUrl); + var parser = new Parser(tokens); + var document = parser.parseDocument(); + return executeRequest(schema, document, + operationName: operationName, + initialValue: initialValue, + variableValues: variableValues); + } + Future> executeRequest( - GraphQLSchema schema, DocumentContext document, String operationName, - {Map variableValues: const {}, initialValue}) async { + GraphQLSchema schema, DocumentContext document, + {String operationName, + Map variableValues: const {}, + initialValue}) async { var operation = getOperation(document, operationName); var coercedVariableValues = coerceVariableValues(schema, operation, variableValues ?? {}); @@ -62,17 +78,20 @@ class GraphQL { OperationDefinitionContext getOperation( DocumentContext document, String operationName) { - var ops = document.definitions.whereType(); + var ops = + document.definitions.where((d) => d is OperationDefinitionContext); if (operationName == null) { return ops.length == 1 - ? ops.first + ? ops.first as OperationDefinitionContext : throw new GraphQLException( - 'Missing required operation "$operationName".'); + 'This document does not define any operations.'); } else { - return ops.firstWhere((d) => d.name == operationName, - orElse: () => throw new GraphQLException( - 'Missing required operation "$operationName".')); + return ops.firstWhere( + (d) => (d as OperationDefinitionContext).name == operationName, + orElse: () => throw new GraphQLException( + 'Missing required operation "$operationName".')) + as OperationDefinitionContext; } } @@ -139,8 +158,9 @@ class GraphQL { for (var field in fields) { var fieldName = field.field.fieldName.name; - var fieldType = - objectType.fields.firstWhere((f) => f.name == fieldName)?.type; + var fieldType = objectType.fields + .firstWhere((f) => f.name == fieldName, orElse: () => null) + ?.type; if (fieldType == null) continue; var responseValue = await executeField(document, fieldName, objectType, objectValue, fields, fieldType, variableValues); @@ -211,7 +231,12 @@ class GraphQL { Future resolveFieldValue(GraphQLObjectType objectType, T objectValue, String fieldName, Map argumentValues) async { var field = objectType.fields.firstWhere((f) => f.name == fieldName); - return await field.resolve(objectValue, argumentValues) as T; + + if (field.resolve == null) { + return null; + } else { + return await field.resolve(objectValue, argumentValues) as T; + } } Future completeValue( @@ -375,4 +400,7 @@ class GraphQL { class GraphQLException extends FormatException { GraphQLException(String message) : super(message); + + @override + String toString() => 'GraphQL exception: $message'; } diff --git a/graphql_server/pubspec.yaml b/graphql_server/pubspec.yaml index e51adf56..87e515c5 100644 --- a/graphql_server/pubspec.yaml +++ b/graphql_server/pubspec.yaml @@ -4,4 +4,5 @@ dependencies: path: ../graphql_schema graphql_parser: path: ../graphql_parser - symbol_table: ^1.0.0 \ No newline at end of file +dev_dependencies: + test: ^0.12.0 \ No newline at end of file diff --git a/graphql_server/test/query_test.dart b/graphql_server/test/query_test.dart new file mode 100644 index 00000000..e53331fa --- /dev/null +++ b/graphql_server/test/query_test.dart @@ -0,0 +1,38 @@ +import 'package:graphql_schema/graphql_schema.dart'; +import 'package:graphql_server/graphql_server.dart'; +import 'package:test/test.dart'; + +void main() { + test('todo', () async { + var schema = graphQLSchema( + query: objectType('todo', [ + field( + 'text', + type: graphQLString, + resolve: (obj, args) => obj['text'], + ), + field( + 'completed', + type: graphQLBoolean, + resolve: (obj, args) => obj['completed'], + ), + ]), + ); + + var graphql = new GraphQL(schema); + var result = await graphql.parseAndExecute('{ text }', initialValue: { + 'text': 'Clean your room!', + 'completed': false, + }); + + print(result); + expect(result, {'text': 'Clean your room!'}); + }); +} + +class Todo { + final String text; + final bool completed; + + Todo({this.text, this.completed}); +}